diff --git a/.gitignore b/.gitignore index 79a0053..438ee00 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,9 @@ ENV/ .env.bak/ .conda-env/ +# =============== 静态分析缓存 =============== +.cppcheck-cache/ + # =============== 日志文件 =============== logs/ **/logs/ @@ -149,4 +152,4 @@ temp/ # =============== 其他 =============== HELP.md .mvn/wrapper/maven-wrapper.jar - \ No newline at end of file + *Zone.Identifier diff --git a/CMakeLists.txt b/CMakeLists.txt index c7474db..eacef53 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -216,7 +216,6 @@ if(BUILD_GUI AND HAVE_QT6) src/gui/backup_tab.cpp src/gui/restore_tab.cpp src/gui/schedule_tab.cpp - src/gui/settings_tab.cpp src/gui/backup_worker.cpp src/gui/progress_widget.cpp src/gui/log_widget.cpp @@ -228,11 +227,11 @@ if(BUILD_GUI AND HAVE_QT6) src/gui/backup_tab.h src/gui/restore_tab.h src/gui/schedule_tab.h - src/gui/settings_tab.h src/gui/backup_worker.h src/gui/progress_widget.h src/gui/log_widget.h src/gui/filter_dialog.h + src/gui/gui_utils.h ) set(GUI_RESOURCES src/gui/resources/app.qrc diff --git a/cppcheck-report.txt b/cppcheck-report.txt new file mode 100644 index 0000000..f68fdbc --- /dev/null +++ b/cppcheck-report.txt @@ -0,0 +1,245 @@ +src/core/backup_engine.cpp:146:19: style: Variable 'content' can be declared as reference to const [constVariableReference] + auto& content = readResult.value(); + ^ +src/core/restore_engine.cpp:148:19: style: Variable 'content' can be declared as reference to const [constVariableReference] + auto& content = readResult.value(); + ^ +tests/core/backup_engine_test.cpp:68:1: error: syntax error [syntaxError] +TEST_F(BackupEngineTest, BackupBasic) { +^ +tests/fs/metadata_test.cpp:44:1: error: syntax error [syntaxError] +TEST_F(MetadataTest, ReadMetadataRegular) { +^ +tests/core/restore_engine_test.cpp:77:1: error: syntax error [syntaxError] +TEST_F(RestoreEngineTest, RestoreBasic) { +^ +tests/fs/special_file_test.cpp:43:1: error: syntax error [syntaxError] +TEST_F(SpecialFileTest, DetectRegularFile) { +^ +tests/pack/tar_packer_test.cpp:149:1: error: syntax error [syntaxError] +TEST_F(TarPackerTest, PackSingleRegularFile) { +^ +tests/filter/criteria_filter_test.cpp:35:1: error: syntax error [syntaxError] +TEST(CriteriaFilterTest, IncludePathGlob) +^ +tests/compress/compressor_test.cpp:17:1: error: syntax error [syntaxError] +TEST_P(CompressorTest, RoundTripEmpty) { +^ +tests/compress/compressor_factory_test.cpp:19:1: error: syntax error [syntaxError] +TEST(BuildCompressorTest, CreateZstd) { +^ +tests/pack/zip_packer_test.cpp:151:1: error: syntax error [syntaxError] +TEST_F(ZipPackerTest, FormatName) { +^ +tests/crypto/openssl_encryptor_test.cpp:16:1: error: syntax error [syntaxError] +TEST_P(EncryptorTest, RoundTripEmpty) { +^ +tests/crypto/encryptor_factory_test.cpp:16:1: error: syntax error [syntaxError] +TEST(BuildEncryptorTest, CreateAes256FullName) { +^ +tests/scheduler/ccronexpr_test.cpp:30:1: error: syntax error [syntaxError] +TEST(CcronexprTest, DailyAt2am) { +^ +tests/scheduler/retention_policy_test.cpp:31:1: error: syntax error [syntaxError] +TEST(RetentionTest, ParseTimestampInvalidFormat) { +^ +build/backer-gui_autogen/MXUWEOXILK/moc_backup_tab.cpp:22:0: error: #error "The header file 'backup_tab.h' doesn't include ." [preprocessorErrorDirective] +#error "The header file 'backup_tab.h' doesn't include ." +^ +src/gui/main_window.h:21:9: error: There is an unknown macro here somewhere. Configuration is required. If slots is a macro then please configure it. [unknownMacro] +private slots: + ^ +tests/scheduler/backup_scheduler_test.cpp:21:1: error: syntax error [syntaxError] +TEST(SchedulerTest, AddAndListJobs) { +^ +src/gui/schedule_tab.h:19:9: error: There is an unknown macro here somewhere. Configuration is required. If slots is a macro then please configure it. [unknownMacro] +private slots: + ^ +src/gui/progress_widget.h:17:8: error: There is an unknown macro here somewhere. Configuration is required. If slots is a macro then please configure it. [unknownMacro] +public slots: + ^ +src/gui/backup_tab.h:35:9: error: There is an unknown macro here somewhere. Configuration is required. If slots is a macro then please configure it. [unknownMacro] +private slots: + ^ +src/gui/log_widget.h:17:8: error: There is an unknown macro here somewhere. Configuration is required. If slots is a macro then please configure it. [unknownMacro] +public slots: + ^ +src/gui/restore_tab.h:28:9: error: There is an unknown macro here somewhere. Configuration is required. If slots is a macro then please configure it. [unknownMacro] +private slots: + ^ +src/gui/backup_worker.h:60:8: error: There is an unknown macro here somewhere. Configuration is required. If slots is a macro then please configure it. [unknownMacro] +public slots: + ^ +src/pack/tar_packer.cpp:67:22: warning: Opposite inner 'if' condition leads to a dead code block. [oppositeInnerCondition] + if (slashPos == 0) break; + ^ +src/pack/tar_packer.cpp:56:54: note: outer condition: slashPos>0 + while (slashPos != std::string::npos && slashPos > 0) { + ^ +src/pack/tar_packer.cpp:67:22: note: opposite inner condition: slashPos==0 + if (slashPos == 0) break; + ^ +src/gui/filter_dialog.h:52:9: error: There is an unknown macro here somewhere. Configuration is required. If slots is a macro then please configure it. [unknownMacro] +private slots: + ^ +src/pack/tar_packer.cpp:255:14: style: Variable 'pad' can be declared as const array [constVariable] + char pad[kBlockSize] = {}; + ^ +src/pack/tar_packer.cpp:345:10: style: Variable 'end' can be declared as const array [constVariable] + char end[kBlockSize * 2] = {}; + ^ +src/crypto/openssl_encryptor.cpp:26:27: style: The scope of the variable 'buf' can be reduced. [variableScope] + std::array buf{}; + ^ +src/filters/criteria_filter.cpp:202:74: style: Consider using std::any_of algorithm instead of a raw loop. [useStlAlgorithm] + && matchGlob(pathStr, c.pathGlob.value(), true)) { + ^ +src/filters/criteria_filter.cpp:214:62: style: Consider using std::any_of algorithm instead of a raw loop. [useStlAlgorithm] + && entry.type == c.fileType.value()) { + ^ +src/filters/criteria_filter.cpp:227:75: style: Consider using std::any_of algorithm instead of a raw loop. [useStlAlgorithm] + && matchGlob(nameStr, c.nameGlob.value(), false)) { + ^ +src/filters/criteria_filter.cpp:275:73: style: Consider using std::any_of algorithm instead of a raw loop. [useStlAlgorithm] + && entry.metadata.ownerId == c.ownerId.value()) { + ^ +src/scheduler/backup_scheduler.cpp:58:45: style: Consider using std::count_if algorithm instead of a raw loop. [useStlAlgorithm] + for (char c : cronExpr) { if (c == ' ') fieldCount++; } + ^ +src/main.cpp:30:0: style: Consider using std::any_of algorithm instead of a raw loop. [useStlAlgorithm] + if (v == val) return std::string{}; +^ +src/cli/commands.cpp:226:24: style: Condition 'resolveUser(options.owner,uid)' is always false [knownConditionTrueFalse] + if (resolveUser(options.owner, uid)) { + ^ +src/cli/commands.cpp:226:24: note: Calling function 'resolveUser' returns 0 + if (resolveUser(options.owner, uid)) { + ^ +src/cli/commands.cpp:226:24: note: Condition 'resolveUser(options.owner,uid)' is always false + if (resolveUser(options.owner, uid)) { + ^ +src/cli/commands.cpp:108:57: style: Parameter 'uid' can be declared as reference to const [constParameterReference] +bool resolveUser(std::string const& userName, uint32_t& uid) { + ^ +src/cli/commands.cpp:728:34: warning: Either the condition 'args.size()<4' is redundant or size of 'args' can be 4. Expression 'args[4]' causes access out of bounds. [containerOutOfBounds] + job.destination = args[4]; + ^ +src/cli/commands.cpp:717:25: note: Assuming that condition 'args.size()<4' is not redundant + if (args.size() < 4) { + ^ +src/cli/commands.cpp:728:34: note: Access out of bounds + job.destination = args[4]; + ^ +src/cli/commands.cpp:770:29: style: Consider using std::replace_if algorithm instead of a raw loop. [useStlAlgorithm] + if (c == ' ') c = '_'; + ^ +src/cli/commands.cpp:776:13: style: Consider using std::any_of algorithm instead of a raw loop. [useStlAlgorithm] + for (auto const& j : jobs) if (j.id == id) return true; + ^ +src/cli/commands.cpp:838:34: style: Consider using std::find_if algorithm instead of a raw loop. [useStlAlgorithm] + if (j.id == args[1]) { + ^ +src/scheduler/backup_scheduler.cpp:118:0: style: The function 'addJob' is never used. [unusedFunction] +BackupScheduler::addJob(ScheduleJob const& job) { +^ +src/core/backup_engine.cpp:20:0: style: The function 'backup' is never used. [unusedFunction] +BackupResult BackupEngine::backup( +^ +src/fs/path_mapper.cpp:22:0: style: The function 'backupToRelative' is never used. [unusedFunction] +std::filesystem::path PathMapper::backupToRelative( +^ +src/compress/build_compressor.cpp:8:0: style: The function 'buildCompressor' is never used. [unusedFunction] +std::unique_ptr buildCompressor(std::string_view name, int level) { +^ +src/crypto/build_encryptor.cpp:6:0: style: The function 'buildEncryptor' is never used. [unusedFunction] +std::unique_ptr buildEncryptor(std::string_view name) { +^ +src/compress/lzma_compressor.cpp:13:0: style: The function 'compress' is never used. [unusedFunction] +Expected LzmaCompressor::compress( +^ +src/compress/lzma_compressor.cpp:56:0: style: The function 'decompress' is never used. [unusedFunction] +Expected LzmaCompressor::decompress( +^ +src/crypto/openssl_encryptor.cpp:217:0: style: The function 'decrypt' is never used. [unusedFunction] +Expected OpenSslEncryptor::decrypt( +^ +src/compress/lzma_compressor.cpp:19:0: style: The function 'defaultLevel' is never used. [unusedFunction] + +^ +src/fs/special_file.cpp:21:0: style: The function 'detectFileType' is never used. [unusedFunction] +Expected detectFileType(std::filesystem::path const& path) +^ +src/scheduler/backup_scheduler.cpp:163:0: style: The function 'enableJob' is never used. [unusedFunction] +BackupScheduler::enableJob(std::string_view jobId, bool enabled) { +^ +src/crypto/openssl_encryptor.cpp:109:0: style: The function 'encrypt' is never used. [unusedFunction] +Expected OpenSslEncryptor::encrypt( +^ +src/scheduler/schedule_store.cpp:32:0: style: The function 'filePath' is never used. [unusedFunction] + default: out += c; +^ +src/storage/local_storage.cpp:159:0: style: The function 'fileSize' is never used. [unusedFunction] +LocalStorage::fileSize(std::filesystem::path const& path) +^ +src/scheduler/backup_scheduler.cpp:190:0: style: The function 'findJob' is never used. [unusedFunction] +ScheduleJob const* BackupScheduler::findJob(std::string_view jobId) const { +^ +src/storage/local_storage.cpp:42:0: style: The function 'isDirectory' is never used. [unusedFunction] +Expected LocalStorage::isDirectory( +^ +src/main.cpp:104:0: style: The function 'isRunning' is never used. [unusedFunction] + ->check([&](std::string const& /*val*/) { backupHasSizeMin = true; return std::string{}; }); +^ +src/compress/lzma_compressor.cpp:9:0: style: The function 'isValidLevel' is never used. [unusedFunction] +bool LzmaCompressor::isValidLevel(int level) const { +^ +src/storage/local_storage.cpp:71:0: style: The function 'listDirectory' is never used. [unusedFunction] +LocalStorage::listDirectory(std::filesystem::path const& path) +^ +src/scheduler/backup_scheduler.cpp:200:0: style: The function 'listJobs' is never used. [unusedFunction] +std::vector BackupScheduler::listJobs() const { +^ +src/scheduler/backup_scheduler.cpp:86:0: style: The function 'makeSnapshotPath' is never used. [unusedFunction] +BackupScheduler::makeSnapshotPath(std::filesystem::path const& base) { +^ +src/fs/metadata.cpp:192:0: style: The function 'metadataFromJson' is never used. [unusedFunction] +std::optional metadataFromJson(std::string const& json) +^ +src/fs/metadata.cpp:176:0: style: The function 'metadataToJson' is never used. [unusedFunction] +std::string metadataToJson(Metadata const& meta) +^ +src/crypto/key_derivation.cpp:56:0: style: The function 'randomBytes' is never used. [unusedFunction] +Expected, ErrorCode> KeyDerivation::randomBytes(size_t length) { +^ +src/storage/local_storage.cpp:96:0: style: The function 'readFile' is never used. [unusedFunction] +LocalStorage::readFile(std::filesystem::path const& path) +^ +src/fs/special_file.cpp:61:0: style: The function 'readSymlink' is never used. [unusedFunction] +Expected readSymlink(std::filesystem::path const& path) +^ +src/scheduler/backup_scheduler.cpp:146:0: style: The function 'removeJob' is never used. [unusedFunction] +BackupScheduler::removeJob(std::string_view jobId) { +^ +src/core/restore_engine.cpp:19:0: style: The function 'restore' is never used. [unusedFunction] +BackupResult RestoreEngine::restore( +^ +src/scheduler/backup_scheduler.cpp:290:0: style: The function 'run' is never used. [unusedFunction] +void BackupScheduler::run() { +^ +src/scheduler/schedule_store.cpp:290:0: style: The function 'save' is never used. [unusedFunction] +ScheduleStore::save(std::vector const& jobs) { +^ +src/scheduler/retention_policy.cpp:61:0: style: The function 'scanSnapshots' is never used. [unusedFunction] +RetentionPolicy::scanSnapshots(std::filesystem::path const& directory) { +^ +src/scheduler/retention_policy.cpp:90:0: style: The function 'selectForRemoval' is never used. [unusedFunction] +RetentionPolicy::selectForRemoval( +^ +src/crypto/build_encryptor.cpp:34:0: style: The function 'suffix' is never used. [unusedFunction] + +^ +src/storage/local_storage.cpp:126:0: style: The function 'writeFile' is never used. [unusedFunction] +LocalStorage::writeFile( +^ +nofile:0:0: information: Active checkers: There was critical errors (use --checkers-report= to see details) [checkersReport] + diff --git a/cppcheck-suppressions.txt b/cppcheck-suppressions.txt new file mode 100644 index 0000000..09e7a98 --- /dev/null +++ b/cppcheck-suppressions.txt @@ -0,0 +1,41 @@ +# ========== Backer Project cppcheck Suppressions ========== +# 格式:[error-id]:[file-pattern]:[line] +# file-pattern 中 * 通配任意字符 + +# ========== 第三方依赖(FetchContent 拉取),__deps 目录全部跳过 ========== +*:build/_deps/* + +# ========== 第三方库已知风格问题 ========== +# ccronexpr — C 代码库,不动 +*:src/scheduler/ccronexpr.c +*:src/scheduler/ccronexpr.h + +# ========== GUI 相关 ========== +# Qt moc 生成的调用 cppcheck 看不见 +unusedFunction:src/gui/* +# Qt 信号/槽参数风格 +constParameter:src/gui/* +constVariable:src/gui/* +constParameterReference:src/gui/* +# Qt 元对象系统 +noOperatorEq:src/gui/* +noCopyConstructor:src/gui/* +noDestructor:src/gui/* +uninitMemberVar:src/gui/* + +# ========== 自研基础设施(设计意图明确) ========== +# Expected 的隐式构造函数是故意设计,用于便捷返回 T 或 E +noExplicitConstructor:src/core/expected.h +# span 的隐式构造遵循 std::span 惯例 +noExplicitConstructor:src/core/span.h + +# ========== 已知误报 ========== +# criteria_filter.cpp: queryDims() 在初始化赋值后通过 FilterQuery 填充这些变量 +# cppcheck 静态分析看不到过滤查询的运行时效果 +knownConditionTrueFalse:src/filters/criteria_filter.cpp + +# ========== informational(非告警,--enable=all 默认包含) ========== +normalCheckLevelMaxBranches:* + +# ========== 系统头 ========== +missingIncludeSystem diff --git a/src/gui/backup_tab.cpp b/src/gui/backup_tab.cpp index 82b9b0d..264d797 100644 --- a/src/gui/backup_tab.cpp +++ b/src/gui/backup_tab.cpp @@ -2,11 +2,13 @@ #include "gui/backup_worker.h" #include "gui/filter_dialog.h" #include "gui/gui_style.h" +#include "gui/gui_utils.h" #include "gui/log_widget.h" #include "gui/progress_widget.h" #include #include +#include #include #include #include @@ -110,18 +112,102 @@ void BackupTab::setupUi() // ── Filter detail group ─────────────────────────────────── filterGroup_ = new QGroupBox(QStringLiteral("文件筛选")); auto* filterForm = new QFormLayout(filterGroup_); + + // Row 1-2: Path includePaths_ = new QLineEdit(); - includePaths_->setPlaceholderText(QStringLiteral("用逗号分隔的包含路径")); + includePaths_->setPlaceholderText(QStringLiteral("用逗号分隔的包含路径, 如 core, src/*.cpp")); + includePaths_->setToolTip(QStringLiteral( + "路径过滤语法:\n" + " core → 包含 core 目录下所有文件(自动递归)\n" + " *.cpp → 只匹配根目录下的 .cpp 文件\n" + " 多个用逗号分隔 → core, src/*.h\n" + "支持 glob: *, ?, [...]")); excludePaths_ = new QLineEdit(); - excludePaths_->setPlaceholderText(QStringLiteral("用逗号分隔的排除路径")); - includeTypes_ = new QLineEdit(); - includeTypes_->setPlaceholderText(QStringLiteral("file, dir, symlink, fifo, block, char")); - excludeTypes_ = new QLineEdit(); - excludeTypes_->setPlaceholderText(QStringLiteral("file, dir, symlink, fifo, block, char")); + excludePaths_->setPlaceholderText(QStringLiteral("用逗号分隔的排除路径, 如 *.tmp, build/")); + excludePaths_->setToolTip(includePaths_->toolTip()); filterForm->addRow(QStringLiteral("包含路径:"), includePaths_); filterForm->addRow(QStringLiteral("排除路径:"), excludePaths_); + + // Row 3-4: Type + includeTypes_ = new QLineEdit(); + includeTypes_->setPlaceholderText(QStringLiteral("file, dir, symlink, fifo, block, char, socket — 不是后缀!")); + includeTypes_->setToolTip(QStringLiteral( + "按文件类型过滤(不是后缀名)\n" + "可选值: file, dir, symlink, fifo, block, char, socket\n" + "例: 只备份普通文件 → file\n" + "例: 排除符号链接 → symlink\n\n" + "提示: 按后缀名筛选请用「名称」字段,如 *.txt")); + excludeTypes_ = new QLineEdit(); + excludeTypes_->setPlaceholderText(QStringLiteral("file, dir, symlink, fifo, block, char, socket — 不是后缀!")); + excludeTypes_->setToolTip(includeTypes_->toolTip()); filterForm->addRow(QStringLiteral("包含类型:"), includeTypes_); filterForm->addRow(QStringLiteral("排除类型:"), excludeTypes_); + + // Row 5-6: Name + includeNames_ = new QLineEdit(); + includeNames_->setPlaceholderText(QStringLiteral("用逗号分隔的包含名称 (如 *.txt, *.cpp)")); + excludeNames_ = new QLineEdit(); + excludeNames_->setPlaceholderText(QStringLiteral("用逗号分隔的排除名称 (如 *.tmp, *.log)")); + filterForm->addRow(QStringLiteral("包含名称:"), includeNames_); + filterForm->addRow(QStringLiteral("排除名称:"), excludeNames_); + + // Row 7: Time + auto* timeRow = new QHBoxLayout(); + enableTimeFilter_ = new QCheckBox(QStringLiteral("启用")); + mtimeAfter_ = new QDateTimeEdit(); + mtimeAfter_->setCalendarPopup(true); + mtimeAfter_->setDisplayFormat(QStringLiteral("yyyy-MM-dd HH:mm")); + mtimeAfter_->setDateTime(QDateTime::currentDateTime().addDays(-7)); + mtimeAfter_->setEnabled(false); + mtimeBefore_ = new QDateTimeEdit(); + mtimeBefore_->setCalendarPopup(true); + mtimeBefore_->setDisplayFormat(QStringLiteral("yyyy-MM-dd HH:mm")); + mtimeBefore_->setDateTime(QDateTime::currentDateTime()); + mtimeBefore_->setEnabled(false); + timeRow->addWidget(enableTimeFilter_); + timeRow->addWidget(new QLabel(QStringLiteral("修改时间:"))); + timeRow->addWidget(mtimeAfter_); + timeRow->addWidget(new QLabel(QStringLiteral("→"))); + timeRow->addWidget(mtimeBefore_); + timeRow->addStretch(); + filterForm->addRow(QStringLiteral("时间筛选:"), timeRow); + + // Row 8: Size + auto* sizeRow = new QHBoxLayout(); + enableSizeFilter_ = new QCheckBox(QStringLiteral("启用")); + sizeMin_ = new QSpinBox(); + sizeMin_->setRange(0, 999999999); + sizeMin_->setValue(0); + sizeMin_->setEnabled(false); + sizeMax_ = new QSpinBox(); + sizeMax_->setRange(0, 999999999); + sizeMax_->setValue(0); + sizeMax_->setSpecialValueText(QStringLiteral("不限")); + sizeMax_->setEnabled(false); + sizeUnitMin_ = new QComboBox(); + sizeUnitMin_->addItems({QStringLiteral("B"), QStringLiteral("KB"), + QStringLiteral("MB"), QStringLiteral("GB")}); + sizeUnitMin_->setEnabled(false); + sizeUnitMax_ = new QComboBox(); + sizeUnitMax_->addItems({QStringLiteral("B"), QStringLiteral("KB"), + QStringLiteral("MB"), QStringLiteral("GB")}); + sizeUnitMax_->setEnabled(false); + sizeRow->addWidget(enableSizeFilter_); + sizeRow->addWidget(new QLabel(QStringLiteral("最小:"))); + sizeRow->addWidget(sizeMin_); + sizeRow->addWidget(sizeUnitMin_); + sizeRow->addSpacing(6); + sizeRow->addWidget(new QLabel(QStringLiteral("最大:"))); + sizeRow->addWidget(sizeMax_); + sizeRow->addWidget(sizeUnitMax_); + sizeRow->addStretch(); + filterForm->addRow(QStringLiteral("大小筛选:"), sizeRow); + + // Row 9: Owner + ownerFilter_ = new QLineEdit(); + ownerFilter_->setPlaceholderText(QStringLiteral("用户名或 UID")); + filterForm->addRow(QStringLiteral("所有者:"), ownerFilter_); + filterGroup_->setVisible(false); mainLayout->addWidget(filterGroup_); @@ -169,6 +255,18 @@ void BackupTab::setupConnections() filterGroup_, &QGroupBox::setVisible); connect(enableFilter_, &QCheckBox::toggled, editFilterBtn_, &QPushButton::setEnabled); + connect(enableTimeFilter_, &QCheckBox::toggled, + mtimeAfter_, &QWidget::setEnabled); + connect(enableTimeFilter_, &QCheckBox::toggled, + mtimeBefore_, &QWidget::setEnabled); + connect(enableSizeFilter_, &QCheckBox::toggled, + sizeMin_, &QWidget::setEnabled); + connect(enableSizeFilter_, &QCheckBox::toggled, + sizeMax_, &QWidget::setEnabled); + connect(enableSizeFilter_, &QCheckBox::toggled, + sizeUnitMin_, &QWidget::setEnabled); + connect(enableSizeFilter_, &QCheckBox::toggled, + sizeUnitMax_, &QWidget::setEnabled); } void BackupTab::onBrowseSource() @@ -196,6 +294,14 @@ void BackupTab::onCompressToggled(bool checked) { compressAlgo_->setEnabled(checked); compressLevel_->setEnabled(checked); + if (checked) { + enablePack_->setChecked(true); + if (packFormat_->currentIndex() != 0) { + packFormat_->setCurrentIndex(0); // Tar + logWidget_->appendMessage( + QStringLiteral("已切换至 Tar 打包"), 1); + } + } } void BackupTab::onEncryptToggled(bool checked) @@ -203,14 +309,90 @@ void BackupTab::onEncryptToggled(bool checked) encryptAlgo_->setEnabled(checked); password_->setEnabled(checked); confirmPassword_->setEnabled(checked); + if (checked) { + enablePack_->setChecked(true); + if (packFormat_->currentIndex() != 0) { + packFormat_->setCurrentIndex(0); // Tar + logWidget_->appendMessage( + QStringLiteral("已切换至 Tar 打包"), 1); + } + } } +namespace { +/// Byte multiplier for size-unit combobox index. +qint64 unitMult(int index) noexcept +{ + static constexpr qint64 kUnits[] = {1LL, 1024LL, 1024LL * 1024, 1024LL * 1024 * 1024}; + return (index >= 0 && index < 4) ? kUnits[index] : 1; +} +} // anonymous namespace + void BackupTab::onEditFilter() { FilterDialog dlg(this); + + // Pre-fill ALL dimensions from current inline values + auto split = [](QString const& text) { + return text.split(QRegularExpression(QStringLiteral("[,\\s;]+")), + Qt::SkipEmptyParts); + }; + dlg.setIncludePaths(split(includePaths_->text())); + dlg.setExcludePaths(split(excludePaths_->text())); + dlg.setIncludeNames(split(includeNames_->text())); + dlg.setExcludeNames(split(excludeNames_->text())); + dlg.setIncludeTypes(split(includeTypes_->text())); + dlg.setTimeFilter(enableTimeFilter_->isChecked(), + mtimeAfter_->dateTime(), mtimeBefore_->dateTime()); + { + qint64 minBytes = sizeMin_->value() * unitMult(sizeUnitMin_->currentIndex()); + qint64 maxBytes = sizeMax_->value() * unitMult(sizeUnitMax_->currentIndex()); + dlg.setSizeFilter(enableSizeFilter_->isChecked(), minBytes, maxBytes); + } + dlg.setOwner(ownerFilter_->text()); + if (dlg.exec() == QDialog::Accepted) { + // Sync path/name text fields includePaths_->setText(dlg.includePaths().join(QStringLiteral(", "))); excludePaths_->setText(dlg.excludePaths().join(QStringLiteral(", "))); + includeNames_->setText(dlg.includeNames().join(QStringLiteral(", "))); + excludeNames_->setText(dlg.excludeNames().join(QStringLiteral(", "))); + + // Sync type checkboxes → text field + includeTypes_->setText(dlg.includeTypes().join(QStringLiteral(", "))); + + // Sync time inline controls + enableTimeFilter_->setChecked(dlg.hasTimeFilter()); + mtimeAfter_->setDateTime(dlg.mtimeAfter()); + mtimeBefore_->setDateTime(dlg.mtimeBefore()); + + // Sync size inline controls + enableSizeFilter_->setChecked(dlg.hasSizeFilter()); + qint64 szMin = dlg.sizeMin(); + qint64 szMax = dlg.sizeMax(); + // Find best unit (auto-scale to largest unit >= 1) + int unitIdx = 0; + for (int i = 3; i > 0; --i) { + if (szMin > 0 && szMin % unitMult(i) == 0) { unitIdx = i; break; } + if (szMax > 0 && szMax % unitMult(i) == 0) { unitIdx = i; break; } + } + if (szMin > 0 && szMin % unitMult(unitIdx) == 0) { + sizeMin_->setValue(static_cast(szMin / unitMult(unitIdx))); + sizeUnitMin_->setCurrentIndex(unitIdx); + } else { + sizeMin_->setValue(0); + sizeUnitMin_->setCurrentIndex(0); + } + if (szMax > 0 && szMax % unitMult(unitIdx) == 0) { + sizeMax_->setValue(static_cast(szMax / unitMult(unitIdx))); + sizeUnitMax_->setCurrentIndex(unitIdx); + } else { + sizeMax_->setValue(0); + sizeUnitMax_->setCurrentIndex(0); + } + + // Sync owner + ownerFilter_->setText(dlg.owner()); } } @@ -228,20 +410,65 @@ backer::cli::BackupOptions BackupTab::collectOptions() const opts.compressLevel = compressLevel_->value(); } + if (enableEncrypt_->isChecked()) { + auto algo = encryptAlgo_->currentText(); + if (algo == QStringLiteral("AES-256")) + opts.encryptAlgo = "aes256"; + else if (algo == QStringLiteral("SM4")) + opts.encryptAlgo = "sm4"; + opts.password = password_->text().toStdString(); + } + // Parse filter fields if (enableFilter_->isChecked()) { auto split = [](QString const& text) { return text.split(QRegularExpression(QStringLiteral("[,\\s;]+")), Qt::SkipEmptyParts); }; + auto expandPath = [](QString const& pattern, auto const& addFn) { + if (pattern.contains('*') || pattern.contains('?') || pattern.contains('[')) { + addFn(pattern.toStdString()); + return; + } + // Plain path (no glob) → add patterns for recursive matching + // fnmatch FNM_PATHNAME: * doesn't match /, so generate */ */* */*/* ... + addFn(pattern.toStdString()); // dir itself + for (int depth = 1; depth <= 15; ++depth) { + QString p = pattern; + for (int d = 0; d < depth; ++d) p += QStringLiteral("/*"); + addFn(p.toStdString()); + } + }; for (auto const& s : split(includePaths_->text())) - opts.includePaths.push_back(s.toStdString()); + expandPath(s, [&](auto const& p) { opts.includePaths.push_back(p); }); for (auto const& s : split(excludePaths_->text())) - opts.excludePaths.push_back(s.toStdString()); + expandPath(s, [&](auto const& p) { opts.excludePaths.push_back(p); }); for (auto const& s : split(includeTypes_->text())) opts.includeTypes.push_back(s.toStdString()); for (auto const& s : split(excludeTypes_->text())) opts.excludeTypes.push_back(s.toStdString()); + for (auto const& s : split(includeNames_->text())) + opts.includeNames.push_back(s.toStdString()); + for (auto const& s : split(excludeNames_->text())) + opts.excludeNames.push_back(s.toStdString()); + + // Time filter (inline controls) + if (enableTimeFilter_->isChecked()) { + opts.mtimeAfter = mtimeAfter_->dateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString(); + opts.mtimeBefore = mtimeBefore_->dateTime().toString(QStringLiteral("yyyy-MM-dd HH:mm")).toStdString(); + } + + // Size filter (inline controls) + if (enableSizeFilter_->isChecked()) { + qint64 minBytes = sizeMin_->value() * unitMult(sizeUnitMin_->currentIndex()); + qint64 maxBytes = sizeMax_->value() * unitMult(sizeUnitMax_->currentIndex()); + if (minBytes > 0) { opts.hasSizeMin = true; opts.sizeMin = static_cast(minBytes); } + if (maxBytes > 0) { opts.hasSizeMax = true; opts.sizeMax = static_cast(maxBytes); } + } + + // Owner filter + if (!ownerFilter_->text().trimmed().isEmpty()) + opts.owner = ownerFilter_->text().trimmed().toStdString(); } return opts; @@ -274,6 +501,14 @@ void BackupTab::onStartBackup() } } + // Compression/encryption require packing (operate on single file, not directory) + if (!enablePack_->isChecked() && + (enableCompress_->isChecked() || enableEncrypt_->isChecked())) { + QMessageBox::warning(this, QStringLiteral("提示"), + QStringLiteral("压缩和加密需要打包功能,请勾选「打包」")); + return; + } + // Disable UI during backup startBtn_->setEnabled(false); cancelBtn_->setEnabled(true); @@ -284,7 +519,12 @@ void BackupTab::onStartBackup() auto src = std::filesystem::path(sourcePath_->text().toStdString()); auto dst = std::filesystem::path(destPath_->text().toStdString()); - worker_ = new BackupWorker(BackupWorker::Backup, src, dst, opts, this); + // Create timestamped subdirectory inside chosen destination. + // The backup engine will create the actual dir (mirror mode) or + // write the archive file (pack mode) inside this subpath. + auto backupPath = makeBackupSubPath(dst, src); + + worker_ = new BackupWorker(BackupWorker::Backup, src, backupPath, opts, this); connect(worker_, &BackupWorker::progressUpdated, this, [this](int pct, QString const& file, int done, int total, diff --git a/src/gui/backup_tab.h b/src/gui/backup_tab.h index 5b25667..82c1b24 100644 --- a/src/gui/backup_tab.h +++ b/src/gui/backup_tab.h @@ -8,6 +8,7 @@ class QCheckBox; class QComboBox; +class QDateTimeEdit; class QGroupBox; class QLineEdit; class QPushButton; @@ -57,10 +58,23 @@ private slots: QCheckBox* enableFilter_{nullptr}; QPushButton* editFilterBtn_{nullptr}; QGroupBox* filterGroup_{nullptr}; + + // ── Filter inline controls (all 6 dimensions) ──────────── QLineEdit* includePaths_{nullptr}; QLineEdit* excludePaths_{nullptr}; QLineEdit* includeTypes_{nullptr}; QLineEdit* excludeTypes_{nullptr}; + QLineEdit* includeNames_{nullptr}; + QLineEdit* excludeNames_{nullptr}; + QCheckBox* enableTimeFilter_{nullptr}; + QDateTimeEdit* mtimeAfter_{nullptr}; + QDateTimeEdit* mtimeBefore_{nullptr}; + QCheckBox* enableSizeFilter_{nullptr}; + QSpinBox* sizeMin_{nullptr}; + QComboBox* sizeUnitMin_{nullptr}; + QSpinBox* sizeMax_{nullptr}; + QComboBox* sizeUnitMax_{nullptr}; + QLineEdit* ownerFilter_{nullptr}; // ── Pack / compress / encrypt ───────────────────────────── QCheckBox* enablePack_{nullptr}; diff --git a/src/gui/backup_worker.cpp b/src/gui/backup_worker.cpp index 6983d25..388dfd1 100644 --- a/src/gui/backup_worker.cpp +++ b/src/gui/backup_worker.cpp @@ -117,8 +117,18 @@ void BackupWorker::run() auto const& opts = std::get(options_); resultCode = backer::cli::handleBackup(source_, destination_, opts); } else { - auto const& opts = std::get(options_); - resultCode = backer::cli::handleRestore(source_, destination_, opts); + auto opts = std::get(options_); + if (!opts.password.empty() && opts.decryptAlgo.empty()) { + // Auto-detect: try AES first, fall back to SM4 + opts.decryptAlgo = "aes256"; + resultCode = backer::cli::handleRestore(source_, destination_, opts); + if (resultCode != 0) { + opts.decryptAlgo = "sm4"; + resultCode = backer::cli::handleRestore(source_, destination_, opts); + } + } else { + resultCode = backer::cli::handleRestore(source_, destination_, opts); + } } } catch (std::exception const& e) { errorMsg = e.what(); diff --git a/src/gui/filter_dialog.cpp b/src/gui/filter_dialog.cpp index 6624a31..3891adb 100644 --- a/src/gui/filter_dialog.cpp +++ b/src/gui/filter_dialog.cpp @@ -22,7 +22,7 @@ FilterDialog::FilterDialog(QWidget* parent) : QDialog(parent) { setWindowTitle(QStringLiteral("筛选条件编辑器")); - setMinimumSize(500, 450); + setMinimumSize(560, 600); setupUi(); } @@ -79,6 +79,55 @@ void FilterDialog::setupUi() connect(excludeRemoveBtn, &QPushButton::clicked, this, &FilterDialog::onRemoveExcludePath); + // ── Name filters ────────────────────────────────────────── + auto* nameGroup = new QGroupBox(QStringLiteral("名称筛选")); + auto* nameLayout = new QVBoxLayout(nameGroup); + + // Include names + auto* includeNameLabel = new QLabel(QStringLiteral("包含名称:")); + auto* includeNameRow = new QHBoxLayout(); + includeNameList_ = new QListWidget(); + auto* includeNameAddBtn = new QPushButton(QStringLiteral("+")); + auto* includeNameRemoveBtn = new QPushButton(QStringLiteral("-")); + includeNameAddBtn->setFixedWidth(32); + includeNameRemoveBtn->setFixedWidth(32); + includeNameRow->addWidget(includeNameList_, 1); + auto* includeNameBtnLayout = new QVBoxLayout(); + includeNameBtnLayout->addWidget(includeNameAddBtn); + includeNameBtnLayout->addWidget(includeNameRemoveBtn); + includeNameBtnLayout->addStretch(); + includeNameRow->addLayout(includeNameBtnLayout); + nameLayout->addWidget(includeNameLabel); + nameLayout->addLayout(includeNameRow); + + // Exclude names + auto* excludeNameLabel = new QLabel(QStringLiteral("排除名称:")); + auto* excludeNameRow = new QHBoxLayout(); + excludeNameList_ = new QListWidget(); + auto* excludeNameAddBtn = new QPushButton(QStringLiteral("+")); + auto* excludeNameRemoveBtn = new QPushButton(QStringLiteral("-")); + excludeNameAddBtn->setFixedWidth(32); + excludeNameRemoveBtn->setFixedWidth(32); + excludeNameRow->addWidget(excludeNameList_, 1); + auto* excludeNameBtnLayout = new QVBoxLayout(); + excludeNameBtnLayout->addWidget(excludeNameAddBtn); + excludeNameBtnLayout->addWidget(excludeNameRemoveBtn); + excludeNameBtnLayout->addStretch(); + excludeNameRow->addLayout(excludeNameBtnLayout); + nameLayout->addWidget(excludeNameLabel); + nameLayout->addLayout(excludeNameRow); + + layout->addWidget(nameGroup); + + connect(includeNameAddBtn, &QPushButton::clicked, + this, &FilterDialog::onAddIncludeName); + connect(includeNameRemoveBtn, &QPushButton::clicked, + this, &FilterDialog::onRemoveIncludeName); + connect(excludeNameAddBtn, &QPushButton::clicked, + this, &FilterDialog::onAddExcludeName); + connect(excludeNameRemoveBtn, &QPushButton::clicked, + this, &FilterDialog::onRemoveExcludeName); + // ── Type filters ───────────────────────────────────────── auto* typeGroup = new QGroupBox(QStringLiteral("类型筛选")); auto* typeLayout = new QHBoxLayout(typeGroup); @@ -206,6 +255,63 @@ void FilterDialog::setExcludePaths(QStringList const& paths) excludePathList_->addItem(p); } +void FilterDialog::setIncludeNames(QStringList const& names) +{ + includeNameList_->clear(); + for (auto const& n : names) + includeNameList_->addItem(n); +} + +void FilterDialog::setExcludeNames(QStringList const& names) +{ + excludeNameList_->clear(); + for (auto const& n : names) + excludeNameList_->addItem(n); +} + +void FilterDialog::setIncludeTypes(QStringList const& types) +{ + typeFile_->setChecked(types.contains(QStringLiteral("file"))); + typeDir_->setChecked(types.contains(QStringLiteral("dir"))); + typeSymlink_->setChecked(types.contains(QStringLiteral("symlink"))); + typeFifo_->setChecked(types.contains(QStringLiteral("fifo"))); + typeBlock_->setChecked(types.contains(QStringLiteral("block"))); + typeChar_->setChecked(types.contains(QStringLiteral("char"))); + typeSocket_->setChecked(types.contains(QStringLiteral("socket"))); +} + +void FilterDialog::setTimeFilter(bool enabled, QDateTime const& after, QDateTime const& before) +{ + enableTimeFilter_->setChecked(enabled); + mtimeAfter_->setDateTime(after); + mtimeBefore_->setDateTime(before); +} + +void FilterDialog::setSizeFilter(bool enabled, qint64 minBytes, qint64 maxBytes) +{ + enableSizeFilter_->setChecked(enabled); + // Convert bytes back to best-fit unit + auto guessUnit = [](qint64 bytes) { + if (bytes == 0) return 0; + for (int i = 3; i > 0; --i) { + qint64 mult = unitMultiplier(i); + if (bytes >= mult && bytes % mult == 0) return i; + } + return 0; + }; + int ui = guessUnit(minBytes > 0 ? minBytes : maxBytes); + qint64 mult = unitMultiplier(ui); + sizeMin_->setValue(minBytes > 0 ? static_cast(minBytes / mult) : 0); + sizeUnitMin_->setCurrentIndex(minBytes > 0 ? ui : 0); + sizeMax_->setValue(maxBytes > 0 ? static_cast(maxBytes / mult) : 0); + sizeUnitMax_->setCurrentIndex(maxBytes > 0 ? ui : 0); +} + +void FilterDialog::setOwner(QString const& owner) +{ + owner_->setText(owner); +} + // ── Getters ────────────────────────────────────────────────────── QStringList FilterDialog::includePaths() const @@ -224,6 +330,22 @@ QStringList FilterDialog::excludePaths() const return result; } +QStringList FilterDialog::includeNames() const +{ + QStringList result; + for (int i = 0; i < includeNameList_->count(); ++i) + result << includeNameList_->item(i)->text(); + return result; +} + +QStringList FilterDialog::excludeNames() const +{ + QStringList result; + for (int i = 0; i < excludeNameList_->count(); ++i) + result << excludeNameList_->item(i)->text(); + return result; +} + QStringList FilterDialog::includeTypes() const { QStringList result; @@ -293,7 +415,7 @@ void FilterDialog::onAddIncludePath() bool ok = false; QString text = QInputDialog::getText(this, QStringLiteral("添加包含路径"), - QStringLiteral("路径模式 (支持 glob):"), + QStringLiteral("路径 (支持 glob, 纯路径自动递归):"), QLineEdit::Normal, {}, &ok); if (ok && !text.isEmpty()) includePathList_->addItem(text); @@ -324,4 +446,40 @@ void FilterDialog::onRemoveExcludePath() delete excludePathList_->takeItem(excludePathList_->row(item)); } +void FilterDialog::onAddIncludeName() +{ + bool ok = false; + QString text = QInputDialog::getText(this, + QStringLiteral("添加包含名称"), + QStringLiteral("文件名模式 (支持 glob, 如 *.txt):"), + QLineEdit::Normal, {}, &ok); + if (ok && !text.isEmpty()) + includeNameList_->addItem(text); +} + +void FilterDialog::onRemoveIncludeName() +{ + auto items = includeNameList_->selectedItems(); + for (auto* item : items) + delete includeNameList_->takeItem(includeNameList_->row(item)); +} + +void FilterDialog::onAddExcludeName() +{ + bool ok = false; + QString text = QInputDialog::getText(this, + QStringLiteral("添加排除名称"), + QStringLiteral("文件名模式 (支持 glob, 如 *.tmp):"), + QLineEdit::Normal, {}, &ok); + if (ok && !text.isEmpty()) + excludeNameList_->addItem(text); +} + +void FilterDialog::onRemoveExcludeName() +{ + auto items = excludeNameList_->selectedItems(); + for (auto* item : items) + delete excludeNameList_->takeItem(excludeNameList_->row(item)); +} + } // namespace backer::gui diff --git a/src/gui/filter_dialog.h b/src/gui/filter_dialog.h index 53e2c77..3ac6f3e 100644 --- a/src/gui/filter_dialog.h +++ b/src/gui/filter_dialog.h @@ -24,10 +24,18 @@ class FilterDialog : public QDialog { // ── Setters (pre-fill from existing config) ───────────── void setIncludePaths(QStringList const& paths); void setExcludePaths(QStringList const& paths); + void setIncludeNames(QStringList const& names); + void setExcludeNames(QStringList const& names); + void setIncludeTypes(QStringList const& types); + void setTimeFilter(bool enabled, QDateTime const& after, QDateTime const& before); + void setSizeFilter(bool enabled, qint64 minBytes, qint64 maxBytes); + void setOwner(QString const& owner); // ── Getters ───────────────────────────────────────────── QStringList includePaths() const; QStringList excludePaths() const; + QStringList includeNames() const; + QStringList excludeNames() const; QStringList includeTypes() const; QStringList excludeTypes() const; @@ -46,6 +54,10 @@ private slots: void onRemoveIncludePath(); void onAddExcludePath(); void onRemoveExcludePath(); + void onAddIncludeName(); + void onRemoveIncludeName(); + void onAddExcludeName(); + void onRemoveExcludeName(); private: void setupUi(); @@ -53,6 +65,8 @@ private slots: // ── Path filters ──────────────────────────────────────── QListWidget* includePathList_{nullptr}; QListWidget* excludePathList_{nullptr}; + QListWidget* includeNameList_{nullptr}; + QListWidget* excludeNameList_{nullptr}; // ── Type checkboxes ───────────────────────────────────── QCheckBox* typeFile_{nullptr}; diff --git a/src/gui/gui_utils.h b/src/gui/gui_utils.h new file mode 100644 index 0000000..22ba00a --- /dev/null +++ b/src/gui/gui_utils.h @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace backer::gui { + +/// Current local time as YYYYMMDD_HHMMSS string. +inline std::string currentTimestamp() +{ + auto now = std::chrono::system_clock::now(); + auto tt = std::chrono::system_clock::to_time_t(now); + std::tm tm{}; +#if BACKER_PLATFORM_POSIX + localtime_r(&tt, &tm); +#else + localtime_s(&tm, &tt); +#endif + std::ostringstream os; + os << std::put_time(&tm, "%Y%m%d_%H%M%S"); + return os.str(); +} + +/// Strip known archive/compress/encrypt extensions from a filename. +/// "data_20260714.tar.gz.enc" → "data_20260714" +inline std::string stripExtensions(std::filesystem::path const& path) +{ + static std::unordered_set const kKnownExts = { + ".enc", ".gz", ".zst", ".lzma", ".xz", ".tar", ".zip" + }; + auto s = path.string(); + while (s.size() > 1 && (s.back() == '/' || s.back() == '\\')) + s.pop_back(); + auto filename = std::filesystem::path(s).filename().string(); + auto p = std::filesystem::path(filename); + while (true) { + auto ext = p.extension().string(); + if (!ext.empty() && kKnownExts.count(ext)) { + p = p.stem(); + } else { + break; + } + } + return p.string(); +} + +/// Build a subdirectory name for a backup inside the user-chosen dest dir. +/// makeBackupSubPath("data", "home/user/docs", "tar") +/// → "data/docs_20260715_143000" +inline std::filesystem::path makeBackupSubPath( + std::filesystem::path const& destDir, + std::filesystem::path const& source) +{ + auto srcName = source.filename().string(); + if (srcName.empty() || srcName == "." || srcName == "..") + srcName = "backup"; + auto ts = currentTimestamp(); + return destDir / (srcName + "_" + ts); +} + +/// Build a subdirectory name for a restore inside the user-chosen dest dir. +/// makeRestoreSubPath("data", "backups/docs_20260715_143000.tar.gz") +/// → "data/docs_R20260715_143000" +inline std::filesystem::path makeRestoreSubPath( + std::filesystem::path const& destDir, + std::filesystem::path const& source) +{ + auto baseName = stripExtensions(source); + if (baseName.empty() || baseName == "." || baseName == "..") + baseName = "restore"; + auto ts = currentTimestamp(); + return destDir / (baseName + "_R" + ts); +} + +/// Detected restore parameters inferred from a backup filename's extension chain. +struct DetectedRestore { + bool isEncrypted = false; + std::string decompressAlgo; ///< "gzip", "zstd", "lzma", or empty + std::string packFormat; ///< "tar", "zip", or empty +}; + +/// Parse a backup filename's extension chain to auto-detect restore options. +/// +/// Strips known extensions right-to-left and maps each to a restore parameter: +/// "docs_20260715.tar.gz" → pack=tar, decompress=gzip +/// "docs_20260715.tar.xz.enc" → pack=tar, decompress=lzma, encrypt +/// "docs_20260715.zip" → pack=zip +inline DetectedRestore detectRestoreOptions(std::filesystem::path const& source) +{ + DetectedRestore result; + auto s = source.string(); + // Strip trailing separators + while (s.size() > 1 && (s.back() == '/' || s.back() == '\\')) + s.pop_back(); + auto filename = std::filesystem::path(s).filename().string(); + auto p = std::filesystem::path(filename); + bool done = false; + while (!done && !p.extension().string().empty()) { + auto ext = p.extension().string(); + // Order matters: process right-to-left + if (ext == ".enc") { + result.isEncrypted = true; + } else if (ext == ".gz") { + result.decompressAlgo = "gzip"; + } else if (ext == ".zst") { + result.decompressAlgo = "zstd"; + } else if (ext == ".xz" || ext == ".lzma") { + result.decompressAlgo = "lzma"; + } else if (ext == ".tar") { + result.packFormat = "tar"; + } else if (ext == ".zip") { + result.packFormat = "zip"; + } else { + break; // unknown extension → stop + } + p = p.stem(); + } + return result; +} + +} // namespace backer::gui diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp index af29922..855902a 100644 --- a/src/gui/main_window.cpp +++ b/src/gui/main_window.cpp @@ -2,8 +2,6 @@ #include "gui/backup_tab.h" // backupTab_ type #include "gui/restore_tab.h" // restoreTab_ type #include "gui/schedule_tab.h" // scheduleTab_ type -#include "gui/settings_tab.h" // settingsTab_ type - #include #include #include @@ -35,21 +33,29 @@ void MainWindow::setupUi() backupTab_ = new BackupTab(); restoreTab_ = new RestoreTab(); scheduleTab_ = new ScheduleTab(); - settingsTab_ = new SettingsTab(); setWindowIcon(QIcon(QStringLiteral(":/icons/app"))); tabWidget_->addTab(backupTab_, QIcon(QStringLiteral(":/icons/backup")), QStringLiteral("备份")); tabWidget_->addTab(restoreTab_, QIcon(QStringLiteral(":/icons/restore")), QStringLiteral("还原")); tabWidget_->addTab(scheduleTab_, QIcon(QStringLiteral(":/icons/schedule")), QStringLiteral("定时任务")); - tabWidget_->addTab(settingsTab_, QIcon(QStringLiteral(":/icons/settings")), QStringLiteral("设置")); setCentralWidget(tabWidget_); connect(backupTab_, &BackupTab::backupFinished, this, &MainWindow::onBackupFinished); connect(restoreTab_, &RestoreTab::restoreFinished, - this, &MainWindow::onBackupFinished); + this, [this](bool success, QString const& message) { + if (success) { + QMessageBox::information(this, + QStringLiteral("还原完成"), message); + tabWidget_->setCurrentWidget(backupTab_); + } else { + QMessageBox::critical(this, + QStringLiteral("还原失败"), message); + // Stay on restore tab for retry + } + }); } void MainWindow::setupMenuBar() diff --git a/src/gui/main_window.h b/src/gui/main_window.h index 6fae585..9f1a25a 100644 --- a/src/gui/main_window.h +++ b/src/gui/main_window.h @@ -10,10 +10,9 @@ namespace backer::gui { class BackupTab; class RestoreTab; class ScheduleTab; -class SettingsTab; /// Main application window with tabbed interface for backup, restore, -/// scheduled tasks, and settings. +/// and scheduled tasks. class MainWindow : public QMainWindow { Q_OBJECT public: @@ -32,7 +31,6 @@ private slots: BackupTab* backupTab_{nullptr}; RestoreTab* restoreTab_{nullptr}; ScheduleTab* scheduleTab_{nullptr}; - SettingsTab* settingsTab_{nullptr}; }; } // namespace backer::gui diff --git a/src/gui/resources/app.qrc b/src/gui/resources/app.qrc index b3a73f3..d1d29df 100644 --- a/src/gui/resources/app.qrc +++ b/src/gui/resources/app.qrc @@ -4,7 +4,6 @@ icons/app.png icons/backup.png icons/restore.png - icons/settings.png icons/schedule.png icons/splash.png icons/empty-state.png diff --git a/src/gui/resources/icons/settings.png b/src/gui/resources/icons/settings.png deleted file mode 100644 index 880e74f..0000000 Binary files a/src/gui/resources/icons/settings.png and /dev/null differ diff --git a/src/gui/restore_tab.cpp b/src/gui/restore_tab.cpp index 96aaf39..50c8213 100644 --- a/src/gui/restore_tab.cpp +++ b/src/gui/restore_tab.cpp @@ -1,11 +1,10 @@ #include "gui/restore_tab.h" #include "gui/backup_worker.h" #include "gui/gui_style.h" +#include "gui/gui_utils.h" #include "gui/log_widget.h" #include "gui/progress_widget.h" -#include -#include #include #include #include @@ -51,39 +50,14 @@ void RestoreTab::setupUi() connect(browseSrcBtn, &QPushButton::clicked, this, &RestoreTab::onBrowseSource); connect(browseDestBtn, &QPushButton::clicked, this, &RestoreTab::onBrowseDest); - // ── Decompress / Pack options ───────────────────────────── - auto* optionsLayout = new QHBoxLayout(); - enableDecompress_ = new QCheckBox(QStringLiteral("解压缩")); - decompressAlgo_ = new QComboBox(); - decompressAlgo_->addItems({QStringLiteral("gzip"), QStringLiteral("zstd"), QStringLiteral("lzma")}); - decompressAlgo_->setEnabled(false); - enablePack_ = new QCheckBox(QStringLiteral("打包格式")); - packFormat_ = new QComboBox(); - packFormat_->addItems({QStringLiteral("Tar"), QStringLiteral("Zip")}); - packFormat_->setEnabled(false); - optionsLayout->addWidget(enableDecompress_); - optionsLayout->addWidget(decompressAlgo_); - optionsLayout->addSpacing(12); - optionsLayout->addWidget(enablePack_); - optionsLayout->addWidget(packFormat_); - optionsLayout->addStretch(); - mainLayout->addLayout(optionsLayout); - - connect(enableDecompress_, &QCheckBox::toggled, - decompressAlgo_, &QComboBox::setEnabled); - connect(enablePack_, &QCheckBox::toggled, - packFormat_, &QComboBox::setEnabled); - - // ── Restore flags ───────────────────────────────────────── - auto* flagLayout = new QHBoxLayout(); - preserveMetadata_ = new QCheckBox(QStringLiteral("保留元数据")); - preserveMetadata_->setChecked(true); - handleSpecial_ = new QCheckBox(QStringLiteral("处理特殊文件")); - handleSpecial_->setChecked(true); - flagLayout->addWidget(preserveMetadata_); - flagLayout->addWidget(handleSpecial_); - flagLayout->addStretch(); - mainLayout->addLayout(flagLayout); + // ── Password (only needed for encrypted backups) ────────── + auto* pwdRow = new QHBoxLayout(); + pwdRow->addWidget(new QLabel(QStringLiteral("解密密码:"))); + password_ = new QLineEdit(); + password_->setEchoMode(QLineEdit::Password); + password_->setPlaceholderText(QStringLiteral("加密备份时设置的密码(可选)")); + pwdRow->addWidget(password_, 1); + mainLayout->addLayout(pwdRow); // ── Action buttons ──────────────────────────────────────── auto* btnLayout = new QHBoxLayout(); @@ -144,29 +118,41 @@ void RestoreTab::onStartRestore() QStringLiteral("请选择备份源和还原目标目录")); return; } - if (!std::filesystem::exists(sourcePath_->text().toStdString())) { + auto srcStr = sourcePath_->text().toStdString(); + if (!std::filesystem::exists(srcStr)) { QMessageBox::warning(this, QStringLiteral("提示"), QStringLiteral("备份源路径不存在")); return; } + auto src = std::filesystem::path(srcStr); + auto dst = std::filesystem::path(destPath_->text().toStdString()); + + // Auto-detect all options from the backup filename + auto detected = detectRestoreOptions(src); + if (detected.isEncrypted && password_->text().isEmpty()) { + QMessageBox::warning(this, QStringLiteral("提示"), + QStringLiteral("备份文件已加密,请输入解密密码")); + return; + } + startBtn_->setEnabled(false); cancelBtn_->setEnabled(true); progressWidget_->setRunning(true); logWidget_->clear(); cli::RestoreOptions opts; - opts.preserveMetadata = preserveMetadata_->isChecked(); - opts.handleSpecial = handleSpecial_->isChecked(); - if (enableDecompress_->isChecked()) - opts.decompressAlgo = decompressAlgo_->currentText().toStdString(); - if (enablePack_->isChecked()) - opts.packFormat = packFormat_->currentText().toLower().toStdString(); - - auto src = std::filesystem::path(sourcePath_->text().toStdString()); - auto dst = std::filesystem::path(destPath_->text().toStdString()); + opts.decompressAlgo = detected.decompressAlgo; + opts.packFormat = detected.packFormat; + if (detected.isEncrypted) { + // Leave decryptAlgo empty — BackupWorker will auto-try AES then SM4 + opts.password = password_->text().toStdString(); + } + + // Create timestamped subdirectory inside chosen destination. + auto restorePath = makeRestoreSubPath(dst, src); - worker_ = new BackupWorker(BackupWorker::Restore, src, dst, opts, this); + worker_ = new BackupWorker(BackupWorker::Restore, src, restorePath, opts, this); connect(worker_, &BackupWorker::progressUpdated, this, [this](int pct, QString const& file, int done, int total, @@ -178,7 +164,7 @@ void RestoreTab::onStartRestore() connect(worker_, &BackupWorker::logMessage, logWidget_, &LogWidget::appendMessage); connect(worker_, &BackupWorker::finished, - this, &RestoreTab::onCancel); // re-use onCancel to clean up + this, &RestoreTab::onRestoreFinished); worker_->start(); logWidget_->appendMessage(QStringLiteral("还原任务已启动"), 0); @@ -190,23 +176,28 @@ void RestoreTab::onCancel() worker_->cancel(); cancelBtn_->setEnabled(false); logWidget_->appendMessage(QStringLiteral("正在取消还原..."), 1); - return; } +} - // Cleanup after completion +void RestoreTab::onRestoreFinished(bool success, QString const& msg) +{ startBtn_->setEnabled(true); cancelBtn_->setEnabled(false); progressWidget_->setRunning(false); - QString msg; + if (success) { + progressWidget_->setValue(100); + logWidget_->appendMessage(QStringLiteral("还原完成"), 0); + } else { + logWidget_->appendMessage(QStringLiteral("还原失败: ") + msg, 2); + } + + emit restoreFinished(success, msg); + if (worker_) { - // Use last log line as result hint - msg = QStringLiteral("还原任务已完成"); worker_->deleteLater(); worker_ = nullptr; } - - emit restoreFinished(true, msg); } } // namespace backer::gui diff --git a/src/gui/restore_tab.h b/src/gui/restore_tab.h index 8e67a31..49fde3a 100644 --- a/src/gui/restore_tab.h +++ b/src/gui/restore_tab.h @@ -6,8 +6,6 @@ #include -class QCheckBox; -class QComboBox; class QLineEdit; class QPushButton; @@ -17,8 +15,8 @@ class BackupWorker; class LogWidget; class ProgressWidget; -/// Restore configuration tab — select backup source, target directory, -/// decompression/pack options, and run restore. +/// Restore configuration tab — select backup source and target directory. +/// All format/algorithm detection is automatic from filename. class RestoreTab : public QWidget { Q_OBJECT public: @@ -32,6 +30,7 @@ private slots: void onBrowseDest(); void onStartRestore(); void onCancel(); + void onRestoreFinished(bool success, QString const& msg); private: void setupUi(); @@ -39,12 +38,7 @@ private slots: QLineEdit* sourcePath_{nullptr}; QLineEdit* destPath_{nullptr}; - QCheckBox* enableDecompress_{nullptr}; - QComboBox* decompressAlgo_{nullptr}; - QCheckBox* enablePack_{nullptr}; - QComboBox* packFormat_{nullptr}; - QCheckBox* preserveMetadata_{nullptr}; - QCheckBox* handleSpecial_{nullptr}; + QLineEdit* password_{nullptr}; QPushButton* startBtn_{nullptr}; QPushButton* cancelBtn_{nullptr}; ProgressWidget* progressWidget_{nullptr}; diff --git a/src/gui/settings_tab.cpp b/src/gui/settings_tab.cpp deleted file mode 100644 index c346731..0000000 --- a/src/gui/settings_tab.cpp +++ /dev/null @@ -1,214 +0,0 @@ -#include "gui/settings_tab.h" -#include "gui/gui_style.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace backer::gui { - -SettingsTab::SettingsTab(QWidget* parent) - : QWidget(parent) -{ - setupUi(); - loadSettings(); -} - -void SettingsTab::setupUi() -{ - auto* layout = new QVBoxLayout(this); - layout->setSpacing(10); - - // ── Default paths ───────────────────────────────────────── - auto* pathGroup = new QGroupBox(QStringLiteral("默认路径")); - auto* pathForm = new QFormLayout(pathGroup); - - auto* srcRow = new QHBoxLayout(); - defaultSource_ = new QLineEdit(); - defaultSource_->setPlaceholderText(QStringLiteral("默认源目录")); - auto* browseSrcBtn = new QPushButton(QStringLiteral("浏览")); - srcRow->addWidget(defaultSource_, 1); - srcRow->addWidget(browseSrcBtn); - pathForm->addRow(QStringLiteral("默认源目录:"), srcRow); - - auto* destRow = new QHBoxLayout(); - defaultDest_ = new QLineEdit(); - defaultDest_->setPlaceholderText(QStringLiteral("默认目标目录")); - auto* browseDestBtn = new QPushButton(QStringLiteral("浏览")); - destRow->addWidget(defaultDest_, 1); - destRow->addWidget(browseDestBtn); - pathForm->addRow(QStringLiteral("默认目标目录:"), destRow); - - layout->addWidget(pathGroup); - - connect(browseSrcBtn, &QPushButton::clicked, - this, &SettingsTab::onBrowseDefaultSource); - connect(browseDestBtn, &QPushButton::clicked, - this, &SettingsTab::onBrowseDefaultDest); - - // ── Default options ─────────────────────────────────────── - auto* optGroup = new QGroupBox(QStringLiteral("默认选项")); - auto* optForm = new QFormLayout(optGroup); - - defaultPack_ = new QComboBox(); - defaultPack_->addItems({QStringLiteral("无"), QStringLiteral("Tar"), QStringLiteral("Zip")}); - defaultCompress_ = new QComboBox(); - defaultCompress_->addItems({QStringLiteral("无"), QStringLiteral("gzip"), - QStringLiteral("zstd"), QStringLiteral("lzma")}); - defaultCompressLevel_ = new QSpinBox(); - defaultCompressLevel_->setRange(0, 22); - defaultCompressLevel_->setValue(3); - defaultCompressLevel_->setToolTip(QStringLiteral("0=默认, 1=最快, 22=最佳")); - defaultEncrypt_ = new QComboBox(); - defaultEncrypt_->addItems({QStringLiteral("无"), QStringLiteral("AES-256"), - QStringLiteral("SM4")}); - - optForm->addRow(QStringLiteral("默认打包格式:"), defaultPack_); - optForm->addRow(QStringLiteral("默认压缩算法:"), defaultCompress_); - optForm->addRow(QStringLiteral("压缩级别:"), defaultCompressLevel_); - optForm->addRow(QStringLiteral("默认加密算法:"), defaultEncrypt_); - - layout->addWidget(optGroup); - - // ── Advanced ────────────────────────────────────────────── - auto* advGroup = new QGroupBox(QStringLiteral("高级")); - auto* advForm = new QFormLayout(advGroup); - - logLevel_ = new QComboBox(); - logLevel_->addItems({QStringLiteral("trace"), QStringLiteral("debug"), - QStringLiteral("info"), QStringLiteral("warn"), - QStringLiteral("error")}); - logLevel_->setCurrentIndex(2); // info - threadCount_ = new QSpinBox(); - threadCount_->setRange(1, 16); - threadCount_->setValue(4); - - advForm->addRow(QStringLiteral("日志级别:"), logLevel_); - advForm->addRow(QStringLiteral("并发线程数:"), threadCount_); - - layout->addWidget(advGroup); - - // ── About ───────────────────────────────────────────────── - auto* aboutGroup = new QGroupBox(QStringLiteral("关于")); - auto* aboutLayout = new QVBoxLayout(aboutGroup); - auto* aboutLabel = new QLabel( - QStringLiteral("数据备份软件 v%1\n\n" - "基于 C++17 / Qt 6\n\n" - "计算机组成与体系结构 / 软件工程 课程项目") - .arg(QStringLiteral(BACKER_VERSION))); - aboutLabel->setWordWrap(true); - aboutLabel->setAlignment(Qt::AlignCenter); - aboutLayout->addWidget(aboutLabel); - layout->addWidget(aboutGroup); - - // ── Action buttons ──────────────────────────────────────── - auto* btnLayout = new QHBoxLayout(); - auto* restoreBtn = new QPushButton(QStringLiteral("恢复默认")); - auto* saveBtn = new QPushButton(QStringLiteral("保存设置")); - style::styleButton(saveBtn, QColor(style::kAccentGreen)); - btnLayout->addStretch(); - btnLayout->addWidget(restoreBtn); - btnLayout->addWidget(saveBtn); - layout->addLayout(btnLayout); - layout->addStretch(); - - connect(restoreBtn, &QPushButton::clicked, - this, &SettingsTab::onRestoreDefaults); - connect(saveBtn, &QPushButton::clicked, - this, &SettingsTab::onSave); -} - -void SettingsTab::loadSettings() -{ - QSettings settings(QStringLiteral("backer"), QStringLiteral("backer-gui")); - settings.beginGroup(QStringLiteral("defaults")); - - defaultSource_->setText( - settings.value(QStringLiteral("sourcePath")).toString()); - defaultDest_->setText( - settings.value(QStringLiteral("destPath")).toString()); - - auto pack = settings.value(QStringLiteral("packFormat"), QStringLiteral("无")).toString(); - defaultPack_->setCurrentText(pack); - auto comp = settings.value(QStringLiteral("compressAlgo"), QStringLiteral("无")).toString(); - defaultCompress_->setCurrentText(comp); - defaultCompressLevel_->setValue( - settings.value(QStringLiteral("compressLevel"), 3).toInt()); - auto enc = settings.value(QStringLiteral("encryptAlgo"), QStringLiteral("无")).toString(); - defaultEncrypt_->setCurrentText(enc); - - settings.endGroup(); - - settings.beginGroup(QStringLiteral("advanced")); - auto log = settings.value(QStringLiteral("logLevel"), QStringLiteral("info")).toString(); - logLevel_->setCurrentText(log); - threadCount_->setValue( - settings.value(QStringLiteral("threadCount"), 4).toInt()); - settings.endGroup(); -} - -void SettingsTab::saveSettings() -{ - QSettings settings(QStringLiteral("backer"), QStringLiteral("backer-gui")); - settings.beginGroup(QStringLiteral("defaults")); - settings.setValue(QStringLiteral("sourcePath"), defaultSource_->text()); - settings.setValue(QStringLiteral("destPath"), defaultDest_->text()); - settings.setValue(QStringLiteral("packFormat"), defaultPack_->currentText()); - settings.setValue(QStringLiteral("compressAlgo"), defaultCompress_->currentText()); - settings.setValue(QStringLiteral("compressLevel"), defaultCompressLevel_->value()); - settings.setValue(QStringLiteral("encryptAlgo"), defaultEncrypt_->currentText()); - settings.endGroup(); - - settings.beginGroup(QStringLiteral("advanced")); - settings.setValue(QStringLiteral("logLevel"), logLevel_->currentText()); - settings.setValue(QStringLiteral("threadCount"), threadCount_->value()); - settings.endGroup(); - - settings.sync(); -} - -void SettingsTab::onBrowseDefaultSource() -{ - QString dir = QFileDialog::getExistingDirectory(this, - QStringLiteral("选择默认源目录"), defaultSource_->text()); - if (!dir.isEmpty()) - defaultSource_->setText(dir); -} - -void SettingsTab::onBrowseDefaultDest() -{ - QString dir = QFileDialog::getExistingDirectory(this, - QStringLiteral("选择默认目标目录"), defaultDest_->text()); - if (!dir.isEmpty()) - defaultDest_->setText(dir); -} - -void SettingsTab::onSave() -{ - saveSettings(); - QMessageBox::information(this, QStringLiteral("设置"), - QStringLiteral("设置已保存")); -} - -void SettingsTab::onRestoreDefaults() -{ - defaultSource_->clear(); - defaultDest_->clear(); - defaultPack_->setCurrentIndex(0); - defaultCompress_->setCurrentIndex(0); - defaultCompressLevel_->setValue(3); - defaultEncrypt_->setCurrentIndex(0); - logLevel_->setCurrentIndex(2); - threadCount_->setValue(4); -} - -} // namespace backer::gui diff --git a/src/gui/settings_tab.h b/src/gui/settings_tab.h deleted file mode 100644 index 289332f..0000000 --- a/src/gui/settings_tab.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include - -class QComboBox; -class QLineEdit; -class QPushButton; -class QSpinBox; - -namespace backer::gui { - -/// Settings tab — default paths, compression preferences, log level, etc. -/// Persists settings via QSettings. -class SettingsTab : public QWidget { - Q_OBJECT -public: - explicit SettingsTab(QWidget* parent = nullptr); - -private slots: - void onBrowseDefaultSource(); - void onBrowseDefaultDest(); - void onSave(); - void onRestoreDefaults(); - -private: - void setupUi(); - void loadSettings(); - void saveSettings(); - - QLineEdit* defaultSource_{nullptr}; - QLineEdit* defaultDest_{nullptr}; - QComboBox* defaultPack_{nullptr}; - QComboBox* defaultCompress_{nullptr}; - QSpinBox* defaultCompressLevel_{nullptr}; - QComboBox* defaultEncrypt_{nullptr}; - QComboBox* logLevel_{nullptr}; - QSpinBox* threadCount_{nullptr}; -}; - -} // namespace backer::gui