From db73d38edccbf0d0fa0513a68899ace892af4661 Mon Sep 17 00:00:00 2001 From: Zhang Sheng Date: Wed, 6 May 2026 11:48:40 +0800 Subject: [PATCH 1/4] feat: add dfm-burner CLI tool for optical disc operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Implemented comprehensive command-line interface for DFM burn library 2. Added new dfm-burner executable with subcommands for all major disc operations 3. Includes info query, file burning, ISO operations, disc erasure, and quality checks 4. Supports both interactive progress display and JSON output for scripting 5. Added extensive help documentation and usage examples 6. Implemented UDF packet writing for incremental file operations Log: Added dfm-burner command-line tool for optical disc burning and management Influence: 1. Install dfm-burner binary and verify package dependencies 2. Test each subcommand with various media types (CD/DVD/BD) 3. Verify JSON output format for scripting use cases 4. Test error handling with invalid inputs and device states 5. Check UDF packet writing operations on rewritable media 6. Validate multi-session burning and append functionality feat: 新增dfm-burner光盘操作命令行工具 1. 为DFM刻录库实现全面的命令行接口 2. 新增dfm-burner可执行文件,支持所有主要光盘操作子命令 3. 包含信息查询、文件刻录、ISO操作、光盘擦除和质检功能 4. 支持交互式进度显示和JSON输出以便脚本调用 5. 添加了详尽的帮助文档和使用示例 6. 实现了UDF封包写入功能支持增量文件操作 Log: 新增光盘刻录管理命令行工具dfm-burner Influence: 1. 安装dfm-burner可执行文件并验证软件包依赖 2. 使用不同介质类型(CD/DVD/BD)测试各个子命令 3. 验证脚本调用所需的JSON输出格式 4. 测试无效输入和设备状态下的错误处理 5. 在可擦写介质上测试UDF封包写入操作 6. 验证多区段刻录和追加功能 --- debian/libdfm-burn.install | 1 + debian/libdfm6-burn.install | 1 + src/dfm-burn/dfm-burn-client/CMakeLists.txt | 10 +- src/dfm-burn/dfm-burn-client/README.md | 169 ++++++ src/dfm-burn/dfm-burn-client/burn_utils.cpp | 125 +++++ src/dfm-burn/dfm-burn-client/burn_utils.h | 43 ++ src/dfm-burn/dfm-burn-client/cli_options.cpp | 553 +++++++++++++++++++ src/dfm-burn/dfm-burn-client/cli_options.h | 95 ++++ src/dfm-burn/dfm-burn-client/main.cpp | 462 ++++++++++++---- 9 files changed, 1362 insertions(+), 97 deletions(-) create mode 100644 src/dfm-burn/dfm-burn-client/README.md create mode 100644 src/dfm-burn/dfm-burn-client/burn_utils.cpp create mode 100644 src/dfm-burn/dfm-burn-client/burn_utils.h create mode 100644 src/dfm-burn/dfm-burn-client/cli_options.cpp create mode 100644 src/dfm-burn/dfm-burn-client/cli_options.h diff --git a/debian/libdfm-burn.install b/debian/libdfm-burn.install index 7a3cbf7c..c7b635c0 100644 --- a/debian/libdfm-burn.install +++ b/debian/libdfm-burn.install @@ -1 +1,2 @@ /usr/lib/*/libdfm-burn*.so* +usr/bin/dfm-burner diff --git a/debian/libdfm6-burn.install b/debian/libdfm6-burn.install index dca97207..762a9c34 100644 --- a/debian/libdfm6-burn.install +++ b/debian/libdfm6-burn.install @@ -1 +1,2 @@ /usr/lib/*/libdfm6-burn*.so* +usr/bin/dfm-burner diff --git a/src/dfm-burn/dfm-burn-client/CMakeLists.txt b/src/dfm-burn/dfm-burn-client/CMakeLists.txt index b32dcfaf..242e4a52 100644 --- a/src/dfm-burn/dfm-burn-client/CMakeLists.txt +++ b/src/dfm-burn/dfm-burn-client/CMakeLists.txt @@ -1,9 +1,13 @@ cmake_minimum_required(VERSION 3.10) -project(dfm${DFM_VERSION_MAJOR}-burn-client) +project(dfm-burner) set(SRCS main.cpp + cli_options.cpp + cli_options.h + burn_utils.cpp + burn_utils.h ) find_package(Qt${QT_VERSION_MAJOR}Core REQUIRED) @@ -11,7 +15,7 @@ find_package(Qt${QT_VERSION_MAJOR}Core REQUIRED) add_executable(${PROJECT_NAME} ${SRCS}) include_directories( - ${PROJECT_SOURCE_DIR}/../dfm-burn-lib/inlcude + ${PROJECT_SOURCE_DIR}/../../include ) target_link_libraries( @@ -19,3 +23,5 @@ target_link_libraries( Qt${QT_VERSION_MAJOR}::Core dfm${DFM_VERSION_MAJOR}-burn ) + +install(TARGETS ${PROJECT_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/src/dfm-burn/dfm-burn-client/README.md b/src/dfm-burn/dfm-burn-client/README.md new file mode 100644 index 00000000..49c37ae1 --- /dev/null +++ b/src/dfm-burn/dfm-burn-client/README.md @@ -0,0 +1,169 @@ +# Optical Disc Burner Utilities + +A command-line tool for optical disc operations in Deepin File Manager, powered by libisoburn. + +## Features + +- **Disc information**: Query device, media type, capacity, write speeds +- **Burn files**: Burn files and directories to disc with various filesystem options +- **ISO operations**: Write ISO images to disc, or dump disc content to ISO +- **Disc erase**: Erase rewritable discs (CD-RW, DVD-RW, DVD+RW, BD-RE) +- **Quality check**: Verify disc media quality with detailed statistics +- **Packet writing**: UDF packet writing for incremental file operations + +## Supported Media Types + +CD-ROM, CD-R, CD-RW, DVD-ROM, DVD-R, DVD-RW, DVD+R, DVD+R DL, DVD-R DL, DVD-RAM, DVD+RW, BD-ROM, BD-R, BD-RE + +## Usage + +``` +dfm-burner [options] [arguments] +``` + +### Commands + +| Command | Description | +|---------|-------------| +| `info` | Show optical disc information | +| `burn` | Burn files to disc | +| `write-iso` | Write an ISO image to disc | +| `dump-iso` | Dump disc content to an ISO image | +| `erase` | Erase a rewritable disc | +| `check` | Check media quality | +| `pw` | Packet writing (UDF) operations | + +### info — Show disc information + +``` +dfm-burner info [--json] +``` + +```bash +# Show disc info +dfm-burner info /dev/sr0 + +# JSON output for scripting +dfm-burner info --json /dev/sr0 +``` + +### burn — Burn files to disc + +``` +dfm-burner burn [options] [...] +``` + +Options: +- `--volume-id=` — Set disc label shown in file manager (default: ISOIMAGE) +- `--speed=` — Write speed in CD/DVD multiplier, 0 = auto (default) + +Filesystem (pick one, default is iso9660): +- `--iso9660` — Basic format, works on virtually all systems +- `--joliet` — Long filenames and CJK characters, recommended for Windows +- `--rockridge` — Preserves Linux permissions and symlinks, Linux-only +- `--udf` — Modern format, files > 2GB, Unicode names. Best for mixed-OS + +Behavior: +- `--appendable` — Leave disc open to add more data later (multi-session) +- `--verify` — Read back data after burning to detect errors + +```bash +# Burn a directory +dfm-burner burn /dev/sr0 /home/user/data + +# Burn with custom volume label +dfm-burner burn --volume-id=BACKUP /dev/sr0 /home/user/files + +# Burn with UDF and appendable (multi-session) +dfm-burner burn --udf --appendable /dev/sr0 /home/user/archive + +# Burn with Joliet + Rock Ridge + verification +dfm-burner burn --joliet --rockridge --verify /dev/sr0 file1.txt file2.txt +``` + +### write-iso — Write ISO to disc + +``` +dfm-burner write-iso [--speed=] +``` + +```bash +dfm-burner write-iso /dev/sr0 /home/user/image.iso +dfm-burner write-iso --speed=8 /dev/sr0 /home/user/image.iso +``` + +### dump-iso — Dump disc to ISO + +``` +dfm-burner dump-iso +``` + +```bash +dfm-burner dump-iso /dev/sr0 /home/user/backup.iso +``` + +### erase — Erase disc + +``` +dfm-burner erase +``` + +```bash +dfm-burner erase /dev/sr0 +``` + +### check — Check media quality + +``` +dfm-burner check [--json] +``` + +```bash +dfm-burner check /dev/sr0 +dfm-burner check --json /dev/sr0 +``` + +### pw — Packet writing (UDF) + +``` +dfm-burner pw [additional args] +``` + +Actions: +- `open ` — Open a packet writing session +- `close ` — Close the session +- `put ` — Add a file to the disc +- `mv ` — Rename a file on the disc +- `rm ` — Remove a file from the disc + +```bash +# Open packet writing session +dfm-burner pw open /dev/sr0 /home/user/pw-work + +# Add files incrementally +dfm-burner pw put /dev/sr0 /home/user/pw-work /home/user/data.txt +dfm-burner pw put /dev/sr0 /home/user/pw-work /home/user/photo.jpg + +# Rename a file on disc +dfm-burner pw mv /dev/sr0 /home/user/pw-work old_name.txt new_name.txt + +# Remove a file from disc +dfm-burner pw rm /dev/sr0 /home/user/pw-work unwanted.txt + +# Close session +dfm-burner pw close /dev/sr0 /home/user/pw-work +``` + +## Implementation + +The tool wraps all public APIs of the dfm-burn library: + +- `DOpticalDiscInfo` — Disc properties and capabilities +- `DOpticalDiscManager` — Burn, erase, check, and ISO operations +- `DPacketWritingController` — UDF packet writing operations + +Async operations (burn, write-iso, dump-iso, erase, check) display real-time progress via Qt's event loop. Sync operations (info, pw-*) return immediately. + +## License + +GPL-3.0-or-later diff --git a/src/dfm-burn/dfm-burn-client/burn_utils.cpp b/src/dfm-burn/dfm-burn-client/burn_utils.cpp new file mode 100644 index 00000000..81366f88 --- /dev/null +++ b/src/dfm-burn/dfm-burn-client/burn_utils.cpp @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "burn_utils.h" + +DFM_BURN_BEGIN_NS + +QString formatSize(quint64 bytes) +{ + if (bytes == 0) + return "0 B"; + + const QStringList units = { "B", "KB", "MB", "GB", "TB" }; + int idx = 0; + double size = bytes; + + while (size >= 1024.0 && idx < units.size() - 1) { + size /= 1024.0; + ++idx; + } + + if (idx == 0) + return QString("%1 B").arg(bytes); + + return QString("%1 %2").arg(size, 0, 'f', 1).arg(units.at(idx)); +} + +QString mediaTypeName(MediaType type) +{ + switch (type) { + case MediaType::kNoMedia: + return "No Media"; + case MediaType::kCD_ROM: + return "CD-ROM"; + case MediaType::kCD_R: + return "CD-R"; + case MediaType::kCD_RW: + return "CD-RW"; + case MediaType::kDVD_ROM: + return "DVD-ROM"; + case MediaType::kDVD_R: + return "DVD-R"; + case MediaType::kDVD_RW: + return "DVD-RW"; + case MediaType::kDVD_PLUS_R: + return "DVD+R"; + case MediaType::kDVD_PLUS_R_DL: + return "DVD+R DL"; + case MediaType::kDVD_R_DL: + return "DVD-R DL"; + case MediaType::kDVD_RAM: + return "DVD-RAM"; + case MediaType::kDVD_PLUS_RW: + return "DVD+RW"; + case MediaType::kBD_ROM: + return "BD-ROM"; + case MediaType::kBD_R: + return "BD-R"; + case MediaType::kBD_RE: + return "BD-RE"; + } + return "Unknown"; +} + +QString jobStatusName(JobStatus status) +{ + switch (status) { + case JobStatus::kIdle: + return "Idle"; + case JobStatus::kRunning: + return "Running"; + case JobStatus::kStalled: + return "Stalled"; + case JobStatus::kFinished: + return "Finished"; + case JobStatus::kFailed: + return "Failed"; + } + return "Unknown"; +} + +QString burnOptionsSummary(BurnOptions options) +{ + QStringList parts; + + // Filesystem + if (options.testFlag(BurnOption::kUDF102Supported)) + parts << "UDF filesystem"; + else if (options.testFlag(BurnOption::kJolietSupport) && options.testFlag(BurnOption::kRockRidgeSupport)) + parts << "ISO9660 + Joliet + RockRidge"; + else if (options.testFlag(BurnOption::kJolietSupport)) + parts << "ISO9660 + Joliet"; + else if (options.testFlag(BurnOption::kRockRidgeSupport)) + parts << "ISO9660 + RockRidge"; + else + parts << "ISO9660 filesystem"; + + // Behavior + if (options.testFlag(BurnOption::kKeepAppendable)) + parts << "multi-session (disc stays open)"; + if (options.testFlag(BurnOption::kVerifyDatas)) + parts << "verify after burn"; + + return parts.join(", "); +} + +/** + * @brief Check if a media type is rewritable + */ +bool isRewritable(MediaType type) +{ + switch (type) { + case MediaType::kCD_RW: + case MediaType::kDVD_RW: + case MediaType::kDVD_PLUS_RW: + case MediaType::kDVD_RAM: + case MediaType::kBD_RE: + return true; + default: + return false; + } +} + +DFM_BURN_END_NS diff --git a/src/dfm-burn/dfm-burn-client/burn_utils.h b/src/dfm-burn/dfm-burn-client/burn_utils.h new file mode 100644 index 00000000..019ae000 --- /dev/null +++ b/src/dfm-burn/dfm-burn-client/burn_utils.h @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef BURN_UTILS_H +#define BURN_UTILS_H + +#include +#include + +DFM_BURN_BEGIN_NS + +/** + * @brief Format byte size to human-readable string + * @param bytes Size in bytes + * @return Formatted string (e.g., "702.5 MB") + */ +QString formatSize(quint64 bytes); + +/** + * @brief Convert MediaType enum to display name + */ +QString mediaTypeName(MediaType type); + +/** + * @brief Convert JobStatus enum to display name + */ +QString jobStatusName(JobStatus status); + +/** + * @brief Convert BurnOptions flags to human-readable description + * Returns one-line summary like "UDF filesystem, multi-session, verify after burn" + */ +QString burnOptionsSummary(BurnOptions options); + +/** + * @brief Check if a media type is rewritable (can be erased) + */ +bool isRewritable(MediaType type); + +DFM_BURN_END_NS + +#endif // BURN_UTILS_H diff --git a/src/dfm-burn/dfm-burn-client/cli_options.cpp b/src/dfm-burn/dfm-burn-client/cli_options.cpp new file mode 100644 index 00000000..a20690dd --- /dev/null +++ b/src/dfm-burn/dfm-burn-client/cli_options.cpp @@ -0,0 +1,553 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "cli_options.h" + +#include +#include + +DFM_BURN_USE_NS + +using namespace std; + +// ── Main parse entry ─────────────────────────────────────────── + +bool CliOptions::parse(int argc, char *argv[], BurnCliConfig &config) +{ + if (argc < 2) { + printHelp(); + return false; + } + + // Collect remaining arguments (skip program name) + QStringList args; + for (int i = 2; i < argc; ++i) + args.append(QString::fromLocal8Bit(argv[i])); + + QString cmd = QString::fromLocal8Bit(argv[1]); + + if (cmd == "--help" || cmd == "-h") { + printHelp(); + return false; + } + + // ── Route to subcommand parser ── + if (cmd == "info") + return parseInfoArgs(args, config); + if (cmd == "burn") + return parseBurnArgs(args, config); + if (cmd == "write-iso") + return parseWriteIsoArgs(args, config); + if (cmd == "dump-iso") + return parseDumpIsoArgs(args, config); + if (cmd == "erase") + return parseEraseArgs(args, config); + if (cmd == "check") + return parseCheckArgs(args, config); + if (cmd == "pw") { + if (args.isEmpty()) { + cerr << "Error: 'pw' requires a subcommand (open, close, put, mv, rm)" << endl; + cerr << "Run 'dfm-burner pw --help' for details." << endl; + return false; + } + QString pwAction = args.takeFirst(); + return parsePwArgs(pwAction, args, config); + } + + cerr << "Error: Unknown command '" << cmd.toStdString() << "'" << endl; + printHelp(); + return false; +} + +// ── Help output ──────────────────────────────────────────────── + +void CliOptions::printHelp() const +{ + cout << "Usage: dfm-burner [options] [arguments]" << endl; + cout << endl; + cout << "Optical disc burning tool for Deepin File Manager." << endl; + cout << "Use 'dfm-burner info /dev/sr0' to check disc status before burning." << endl; + cout << endl; + cout << "Commands:" << endl; + cout << " info Show disc info (type, capacity, free space)" << endl; + cout << " burn Burn files or folders to disc" << endl; + cout << " write-iso Write an .iso image to disc" << endl; + cout << " dump-iso Read disc and save as .iso image" << endl; + cout << " erase Erase a rewritable disc (CD-RW, DVD-RW, BD-RE)" << endl; + cout << " check Scan disc for read errors" << endl; + cout << " pw Incremental file operations (UDF packet writing)" << endl; + cout << endl; + cout << "Tips:" << endl; + cout << " - Find your device: ls /dev/sr*" << endl; + cout << " - Get started: dfm-burner info /dev/sr0" << endl; + cout << " - Quick burn: dfm-burner burn /dev/sr0 ./my-folder" << endl; + cout << endl; + cout << "Run 'dfm-burner --help' for details." << endl; +} + +void CliOptions::printCommandHelp(BurnCommand cmd) const +{ + switch (cmd) { + case BurnCommand::Info: + cout << "Usage: dfm-burner info [--json] " << endl; + cout << endl; + cout << "Show disc type, capacity, used/free space, and write speeds." << endl; + cout << "Run this first to check what disc is in the drive before burning." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << endl; + cout << "Options:" << endl; + cout << " --json, -j Output in JSON format (for scripting)" << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner info /dev/sr0 # Check disc status" << endl; + cout << " dfm-burner info --json /dev/sr0 # Machine-readable output" << endl; + break; + + case BurnCommand::Burn: + cout << "Usage: dfm-burner burn [options] [...]" << endl; + cout << endl; + cout << "Burn files and directories to an optical disc." << endl; + cout << "Use 'dfm-burner info /dev/sr0' first to check disc type and free space." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << " file_or_dir One or more files or folders to burn." << endl; + cout << endl; + cout << "Options:" << endl; + cout << " --volume-id= Set disc label shown in file manager (default: ISOIMAGE)" << endl; + cout << " --speed= Write speed in CD/DVD multiplier. 0 = auto pick (default)" << endl; + cout << endl; + cout << "Filesystem (pick one, default is iso9660):" << endl; + cout << " --iso9660 Basic format, works on virtually all systems" << endl; + cout << " --joliet Like iso9660 but supports long filenames and CJK" << endl; + cout << " characters, recommended for Windows compatibility" << endl; + cout << " --rockridge Like iso9660 but preserves Linux permissions and" << endl; + cout << " symbolic links, recommended for Linux-only use" << endl; + cout << " --udf Modern format, supports files > 2GB and Unicode" << endl; + cout << " filenames. Best for large files or mixed-OS use." << endl; + cout << " Combine with --appendable for multi-session." << endl; + cout << endl; + cout << "Behavior:" << endl; + cout << " --appendable After burning, leave disc open so you can add more" << endl; + cout << " data in a later session (multi-session disc)" << endl; + cout << " --verify Read back all data after burning to detect errors." << endl; + cout << " Slower but ensures data integrity." << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " # Quick burn with defaults" << endl; + cout << " dfm-burner burn /dev/sr0 ./my-folder" << endl; + cout << endl; + cout << " # Burn with a custom label" << endl; + cout << " dfm-burner burn --volume-id=Photos_2026 /dev/sr0 ./photos" << endl; + cout << endl; + cout << " # Burn a 4GB file (needs UDF)" << endl; + cout << " dfm-burner burn --udf /dev/sr0 ./large-video.mkv" << endl; + cout << endl; + cout << " # Multi-session: burn now, add more files later" << endl; + cout << " dfm-burner burn --udf --appendable /dev/sr0 ./batch1" << endl; + cout << endl; + cout << " # Cross-platform: works on Windows, Linux, macOS" << endl; + cout << " dfm-burner burn --joliet --rockridge --verify /dev/sr0 ./data" << endl; + break; + + case BurnCommand::WriteISO: + cout << "Usage: dfm-burner write-iso [--speed=] " << endl; + cout << endl; + cout << "Write an .iso image file to disc (1:1 copy, disc-at-once)." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << " iso_path Path to the .iso file to burn." << endl; + cout << endl; + cout << "Options:" << endl; + cout << " --speed= Write speed. 0 = auto pick (default)" << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner write-iso /dev/sr0 ./ubuntu-22.04.iso" << endl; + cout << " dfm-burner write-iso --speed=8 /dev/sr0 ./debian-live.iso" << endl; + break; + + case BurnCommand::DumpISO: + cout << "Usage: dfm-burner dump-iso " << endl; + cout << endl; + cout << "Read disc content and save it as an .iso image file (for backup or redistribution)." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << " output_path Where to save the .iso file." << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner dump-iso /dev/sr0 ./backup.iso" << endl; + break; + + case BurnCommand::Erase: + cout << "Usage: dfm-burner erase " << endl; + cout << endl; + cout << "Erase all data on a rewritable disc." << endl; + cout << "Only works on rewritable media: CD-RW, DVD-RW, DVD+RW, BD-RE." << endl; + cout << "CD-R and DVD+R are write-once and cannot be erased." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner erase /dev/sr0" << endl; + break; + + case BurnCommand::Check: + cout << "Usage: dfm-burner check [--json] " << endl; + cout << endl; + cout << "Read the entire disc to detect read errors and report quality statistics." << endl; + cout << "Useful for verifying old backups or checking disc health." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << endl; + cout << "Options:" << endl; + cout << " --json, -j Output in JSON format (for scripting)" << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner check /dev/sr0" << endl; + cout << " dfm-burner check --json /dev/sr0" << endl; + break; + + case BurnCommand::PwOpen: + case BurnCommand::PwClose: + case BurnCommand::PwPut: + case BurnCommand::PwMv: + case BurnCommand::PwRm: + cout << "Usage: dfm-burner pw [extra_args...]" << endl; + cout << endl; + cout << "Packet writing (UDF) lets you use a rewritable disc like a USB drive:" << endl; + cout << "add, rename, or remove individual files without rewriting the whole disc." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Use 'ls /dev/sr*' to find it." << endl; + cout << " working_path Local directory for temporary staging files." << endl; + cout << " Must be on the same filesystem as the files you put." << endl; + cout << endl; + cout << "Actions:" << endl; + cout << " open Open disc for packet writing (must be called first)" << endl; + cout << " close Finish packet writing and finalize disc" << endl; + cout << " put Copy a file from your computer onto the disc" << endl; + cout << " mv Rename a file already on the disc" << endl; + cout << " rm Delete a file from the disc" << endl; + cout << endl; + cout << "Workflow:" << endl; + cout << " 1. dfm-burner pw open /dev/sr0 /tmp/pw-work" << endl; + cout << " 2. dfm-burner pw put /dev/sr0 /tmp/pw-work /home/user/doc.txt" << endl; + cout << " 3. dfm-burner pw put /dev/sr0 /tmp/pw-work /home/user/photo.jpg" << endl; + cout << " 4. dfm-burner pw close /dev/sr0 /tmp/pw-work" << endl; + break; + + default: + printHelp(); + break; + } +} + +// ── Subcommand parsers ───────────────────────────────────────── + +bool CliOptions::parseInfoArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::Info; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::Info); + return false; + } + if (arg == "--json" || arg == "-j") { + config.jsonOutput = true; + } else if (!arg.startsWith('-') && config.device.isEmpty()) { + config.device = arg; + } else { + cerr << "Error: Unknown argument '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.device.isEmpty()) { + cerr << "Error: Device path is required." << endl; + cerr << "Run 'dfm-burner info --help' for usage." << endl; + return false; + } + return true; +} + +bool CliOptions::parseBurnArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::Burn; + bool hasFsOption = false; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::Burn); + return false; + } + if (arg.startsWith("--volume-id=")) { + config.volumeId = arg.mid(12); + if (config.volumeId.isEmpty()) { + cerr << "Error: --volume-id requires a non-empty value." << endl; + return false; + } + } else if (arg.startsWith("--speed=")) { + bool ok = false; + config.speed = arg.mid(8).toInt(&ok); + if (!ok || config.speed < 0) { + cerr << "Error: Invalid speed value '" << arg.mid(8).toStdString() << "'" << endl; + return false; + } + } else if (arg == "--iso9660") { + config.burnOptions |= BurnOption::kISO9660Only; + hasFsOption = true; + } else if (arg == "--joliet") { + config.burnOptions |= BurnOption::kJolietSupport; + hasFsOption = true; + } else if (arg == "--rockridge") { + config.burnOptions |= BurnOption::kRockRidgeSupport; + hasFsOption = true; + } else if (arg == "--udf") { + config.burnOptions |= BurnOption::kUDF102Supported; + hasFsOption = true; + } else if (arg == "--appendable") { + config.burnOptions |= BurnOption::kKeepAppendable; + } else if (arg == "--verify") { + config.burnOptions |= BurnOption::kVerifyDatas; + } else if (!arg.startsWith('-')) { + if (config.device.isEmpty()) + config.device = arg; + else + config.stageFiles << arg; + } else { + cerr << "Error: Unknown option '" << arg.toStdString() << "'" << endl; + return false; + } + } + + // Default filesystem is ISO9660 + if (!hasFsOption) + config.burnOptions |= BurnOption::kISO9660Only; + + if (config.device.isEmpty()) { + cerr << "Error: Device path is required." << endl; + cerr << "Run 'dfm-burner burn --help' for usage." << endl; + return false; + } + if (config.stageFiles.isEmpty()) { + cerr << "Error: At least one file or directory is required." << endl; + cerr << "Run 'dfm-burner burn --help' for usage." << endl; + return false; + } + + // Validate stage files exist + for (const auto &f : config.stageFiles) { + if (!QFileInfo::exists(f)) { + cerr << "Error: File not found: " << f.toStdString() << endl; + return false; + } + } + + return true; +} + +bool CliOptions::parseWriteIsoArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::WriteISO; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::WriteISO); + return false; + } + if (arg.startsWith("--speed=")) { + bool ok = false; + config.speed = arg.mid(8).toInt(&ok); + if (!ok || config.speed < 0) { + cerr << "Error: Invalid speed value '" << arg.mid(8).toStdString() << "'" << endl; + return false; + } + } else if (!arg.startsWith('-')) { + if (config.device.isEmpty()) + config.device = arg; + else if (config.isoPath.isEmpty()) + config.isoPath = arg; + else { + cerr << "Error: Unexpected argument '" << arg.toStdString() << "'" << endl; + return false; + } + } else { + cerr << "Error: Unknown option '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.device.isEmpty() || config.isoPath.isEmpty()) { + cerr << "Error: Device and ISO path are required." << endl; + cerr << "Run 'dfm-burner write-iso --help' for usage." << endl; + return false; + } + if (!QFileInfo::exists(config.isoPath)) { + cerr << "Error: ISO file not found: " << config.isoPath.toStdString() << endl; + return false; + } + return true; +} + +bool CliOptions::parseDumpIsoArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::DumpISO; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::DumpISO); + return false; + } + if (!arg.startsWith('-')) { + if (config.device.isEmpty()) + config.device = arg; + else if (config.outputPath.isEmpty()) + config.outputPath = arg; + else { + cerr << "Error: Unexpected argument '" << arg.toStdString() << "'" << endl; + return false; + } + } else { + cerr << "Error: Unknown option '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.device.isEmpty() || config.outputPath.isEmpty()) { + cerr << "Error: Device and output path are required." << endl; + cerr << "Run 'dfm-burner dump-iso --help' for usage." << endl; + return false; + } + return true; +} + +bool CliOptions::parseEraseArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::Erase; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::Erase); + return false; + } + if (!arg.startsWith('-') && config.device.isEmpty()) { + config.device = arg; + } else { + cerr << "Error: Unexpected argument '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.device.isEmpty()) { + cerr << "Error: Device path is required." << endl; + cerr << "Run 'dfm-burner erase --help' for usage." << endl; + return false; + } + return true; +} + +bool CliOptions::parseCheckArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::Check; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::Check); + return false; + } + if (arg == "--json" || arg == "-j") { + config.jsonOutput = true; + } else if (!arg.startsWith('-') && config.device.isEmpty()) { + config.device = arg; + } else { + cerr << "Error: Unknown argument '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.device.isEmpty()) { + cerr << "Error: Device path is required." << endl; + cerr << "Run 'dfm-burner check --help' for usage." << endl; + return false; + } + return true; +} + +bool CliOptions::parsePwArgs(const QString &pwAction, const QStringList &args, BurnCliConfig &config) const +{ + // Map action to command + if (pwAction == "--help" || pwAction == "-h") { + printCommandHelp(BurnCommand::PwOpen); + return false; + } + + if (pwAction == "open") { + config.command = BurnCommand::PwOpen; + } else if (pwAction == "close") { + config.command = BurnCommand::PwClose; + } else if (pwAction == "put") { + config.command = BurnCommand::PwPut; + } else if (pwAction == "mv") { + config.command = BurnCommand::PwMv; + } else if (pwAction == "rm") { + config.command = BurnCommand::PwRm; + } else { + cerr << "Error: Unknown pw action '" << pwAction.toStdString() << "'" << endl; + cerr << "Valid actions: open, close, put, mv, rm" << endl; + return false; + } + + // Parse positional arguments + for (const auto &arg : args) { + if (arg.startsWith('-')) { + cerr << "Error: Unknown option '" << arg.toStdString() << "'" << endl; + return false; + } + if (config.device.isEmpty()) + config.device = arg; + else if (config.workingPath.isEmpty()) + config.workingPath = arg; + else if (config.command == BurnCommand::PwPut && config.pwFileName.isEmpty()) + config.pwFileName = arg; + else if (config.command == BurnCommand::PwRm && config.pwFileName.isEmpty()) + config.pwFileName = arg; + else if (config.command == BurnCommand::PwMv && config.pwSrcName.isEmpty()) + config.pwSrcName = arg; + else if (config.command == BurnCommand::PwMv && config.pwDestName.isEmpty()) + config.pwDestName = arg; + else { + cerr << "Error: Unexpected argument '" << arg.toStdString() << "'" << endl; + return false; + } + } + + // Validate required arguments + if (config.device.isEmpty() || config.workingPath.isEmpty()) { + cerr << "Error: Device and working path are required." << endl; + cerr << "Run 'dfm-burner pw --help' for usage." << endl; + return false; + } + + if (config.command == BurnCommand::PwPut && config.pwFileName.isEmpty()) { + cerr << "Error: File name is required for 'put' action." << endl; + return false; + } + if (config.command == BurnCommand::PwRm && config.pwFileName.isEmpty()) { + cerr << "Error: File name is required for 'rm' action." << endl; + return false; + } + if (config.command == BurnCommand::PwMv && (config.pwSrcName.isEmpty() || config.pwDestName.isEmpty())) { + cerr << "Error: Source and destination names are required for 'mv' action." << endl; + return false; + } + + return true; +} diff --git a/src/dfm-burn/dfm-burn-client/cli_options.h b/src/dfm-burn/dfm-burn-client/cli_options.h new file mode 100644 index 00000000..14a86c3f --- /dev/null +++ b/src/dfm-burn/dfm-burn-client/cli_options.h @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef CLI_OPTIONS_H +#define CLI_OPTIONS_H + +#include +#include + +DFM_BURN_BEGIN_NS + +/** + * @brief Supported burn subcommands + */ +enum class BurnCommand { + None, + Info, + Burn, + WriteISO, + DumpISO, + Erase, + Check, + PwOpen, + PwClose, + PwPut, + PwMv, + PwRm +}; + +/** + * @brief Parsed CLI configuration + * + * Fields are populated based on the active subcommand; + * unrelated fields are left at their defaults. + */ +struct BurnCliConfig +{ + BurnCommand command = BurnCommand::None; + QString device; + bool jsonOutput = false; + + // burn options + QStringList stageFiles; + QString volumeId = "ISOIMAGE"; + int speed = 0; + BurnOptions burnOptions; + + // write-iso / dump-iso paths + QString isoPath; + QString outputPath; + + // packet writing + QString workingPath; + QString pwSrcName; + QString pwDestName; + QString pwFileName; +}; + +/** + * @brief Command-line option parser with subcommand routing + * + * Supports git-style subcommands: info, burn, write-iso, dump-iso, + * erase, check, pw . + */ +class CliOptions +{ +public: + CliOptions() = default; + ~CliOptions() = default; + + /** + * @brief Parse command-line arguments into config + * @return true on success, false on error (message already printed) + */ + bool parse(int argc, char *argv[], BurnCliConfig &config); + + void printHelp() const; + +private: + void printCommandHelp(BurnCommand cmd) const; + + // ── Per-subcommand parsers ── + bool parseInfoArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseBurnArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseWriteIsoArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseDumpIsoArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseEraseArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseCheckArgs(const QStringList &args, BurnCliConfig &config) const; + bool parsePwArgs(const QString &pwAction, const QStringList &args, BurnCliConfig &config) const; +}; + +DFM_BURN_END_NS + +#endif // CLI_OPTIONS_H diff --git a/src/dfm-burn/dfm-burn-client/main.cpp b/src/dfm-burn/dfm-burn-client/main.cpp index 6d7e6941..185e2eb9 100644 --- a/src/dfm-burn/dfm-burn-client/main.cpp +++ b/src/dfm-burn/dfm-burn-client/main.cpp @@ -3,109 +3,381 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include -#include +#include +#include +#include +#include +#include +#include +#include #include #include #include +#include "cli_options.h" +#include "burn_utils.h" + DFM_BURN_USE_NS -// TODO(zhangs): follow code is test code - -//static void erase(const QString &dev) -//{ -// DOpticalDiscManager manager(dev); -// QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, [](JobStatus status, int progress, QString speed, QStringList message) { -// qDebug() << int(status) << progress << speed << message; -// }); -// manager.erase(); -//} - -//static void showInfo(const QString &dev) -//{ -// QScopedPointer info { DOpticalDiscManager::createOpticalInfo(dev) }; -// qDebug() << info->device(); -// qDebug() << int(info->mediaType()); -// qDebug() << info->writeSpeed(); -// qDebug() << info->volumeName(); -//} - -//static void commit() -//{ -// DOpticalDiscManager manager("/dev/sr0"); -// QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, [](JobStatus status, int progress, QString speed, QStringList message) { -// qDebug() << int(status) << progress << speed << message; -// }); -// manager.setStageFile("/home/zhangs/.cache/deepin/discburn/_dev_sr0"); -// BurnOptions opts; -// opts |= BurnOption::kJolietAndRockRidge; -// opts |= BurnOption::kKeepAppendable; -// manager.commit(opts, 0, "123"); -//} - -//static void commitUDF() -//{ -// DOpticalDiscManager manager("/dev/sr0"); -// QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, [](JobStatus status, int progress, QString speed, QStringList message) { -// qDebug() << int(status) << progress << speed << message; -// }); -// manager.setStageFile("/home/zhangs/Downloads/254111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111.gz"); -// BurnOptions opts; -// opts |= BurnOption::kUDF102Supported; -// opts |= BurnOption::kKeepAppendable; -// manager.commit(opts, 0, "abc123"); -//} - -//static void writeISO() -//{ -// DOpticalDiscManager manager("/dev/sr0"); -// QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, [](JobStatus status, int progress, QString speed, QStringList message) { -// qDebug() << int(status) << progress << speed << message; -// }); -// manager.writeISO("/home/zhangs/Downloads/deb/20200413_214350.iso"); -//} - -//static void check() -//{ -// DOpticalDiscManager manager("/dev/sr0"); -// QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, [](JobStatus status, int progress, QString speed, QStringList message) { -// qInfo() << int(status) << progress << speed << message; -// }); -// double gud, slo, bad; -// manager.checkmedia(&gud, &slo, &bad); -// bool check { true }; -// bool checkRet { !(check && (bad > (2 + 1e-6))) }; -// qDebug() << "check ret" << checkRet; -//} - -//static void dumpISO() -//{ -// DOpticalDiscManager manager("/dev/sr0"); -// QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, [](JobStatus status, int progress, QString speed, QStringList message) { -// qInfo() << int(status) << progress << speed << message; -// }); -// manager.dumpISO("/home/zhangs/tmp/aabb.iso"); -//} - -//static void pw() -//{ -// DPacketWritingController controller("/dev/sr0", "/home/zhangs"); -// controller.open(); -// controller.close(); -// qDebug() << "quit!!!"; -//} +using namespace std; + +// ── Helpers ──────────────────────────────────────────────────── + +/** + * @brief Run an async operation that reports progress via jobStatusChanged. + * + * @param config Parsed CLI config (must have .device set) + * @param app QCoreApplication reference (for event loop) + * @param startOp Lambda that starts the operation; returns false on immediate failure + * @param opName Human-readable operation name for progress display + * @return Exit code (0 = success, 1 = failure) + */ +static int runAsyncJob(const BurnCliConfig &config, QCoreApplication &app, + const function &startOp, + const QString &opName) +{ + DOpticalDiscManager manager(config.device); + int exitCode = 1; + + QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, + [&exitCode, &app, &opName](JobStatus status, int progress, + const QString &speed, const QStringList &message) { + if (status == JobStatus::kRunning) { + cout << "\r" << opName.toStdString() << "... " << progress << "%"; + if (!speed.isEmpty()) + cout << " (" << speed.toStdString() << ")"; + cout << flush; + } else if (status == JobStatus::kFinished) { + cout << "\r" << opName.toStdString() << "... Done. " << endl; + exitCode = 0; + app.quit(); + } else if (status == JobStatus::kFailed) { + cerr << endl + << "Operation failed: " << message.join(" ").toStdString() << endl; + exitCode = 1; + app.quit(); + } else if (status == JobStatus::kStalled) { + cout << "\r" << opName.toStdString() << "... Stalled (waiting for drive)." << flush; + } + }); + + if (!startOp(manager)) + return 1; + + return app.exec(); +} + +// ── Command: info (sync) ────────────────────────────────────── + +static int handleInfo(const BurnCliConfig &config) +{ + QScopedPointer info(DOpticalDiscManager::createOpticalInfo(config.device)); + if (!info) { + cerr << "Error: Failed to get disc info for " << config.device.toStdString() << endl; + return 1; + } + + if (info->mediaType() == MediaType::kNoMedia) { + cerr << "Error: No disc detected in " << config.device.toStdString() << endl; + cerr << " Insert a disc and try again." << endl; + return 1; + } + + if (config.jsonOutput) { + QJsonObject obj; + obj["device"] = info->device(); + obj["mediaType"] = mediaTypeName(info->mediaType()); + obj["blank"] = info->blank(); + obj["volumeName"] = info->volumeName(); + obj["usedSize"] = static_cast(info->usedSize()); + obj["availableSize"] = static_cast(info->availableSize()); + obj["totalSize"] = static_cast(info->totalSize()); + obj["dataBlocks"] = static_cast(info->dataBlocks()); + QJsonArray speeds; + for (const auto &s : info->writeSpeed()) + speeds.append(s); + obj["writeSpeeds"] = speeds; + cout << QJsonDocument(obj).toJson(QJsonDocument::Indented).toStdString(); + } else { + cout << "Device: " << info->device().toStdString() << endl; + cout << "Media Type: " << mediaTypeName(info->mediaType()).toStdString() + << (isRewritable(info->mediaType()) ? " (rewritable)" : " (write-once)") << endl; + cout << "Blank: " << (info->blank() ? "Yes" : "No") << endl; + cout << "Volume Name: " << (info->volumeName().isEmpty() ? "(empty)" : info->volumeName().toStdString()) << endl; + cout << "Used: " << formatSize(info->usedSize()).toStdString() << endl; + cout << "Available: " << formatSize(info->availableSize()).toStdString() << endl; + cout << "Total: " << formatSize(info->totalSize()).toStdString() << endl; + cout << "Data Blocks: " << info->dataBlocks() << endl; + + QStringList speeds = info->writeSpeed(); + cout << "Write Speeds: " << (speeds.isEmpty() ? "(none)" : speeds.join(", ").toStdString()) << endl; + + // Actionable hints + cout << endl; + if (info->blank()) { + cout << ">> Disc is blank and ready to burn." << endl; + cout << " Quick start: dfm-burner burn " << info->device().toStdString() << " ./your-folder" << endl; + } else if (info->availableSize() > 0) { + cout << ">> Disc has free space (" << formatSize(info->availableSize()).toStdString() << ")." << endl; + cout << " To add data: dfm-burner burn --appendable " << info->device().toStdString() << " ./your-folder" << endl; + cout << " To start fresh, erase first: dfm-burner erase " << info->device().toStdString() << endl; + } else { + cout << ">> Disc is full. To reuse, erase first: dfm-burner erase " << info->device().toStdString() << endl; + } + } + + return 0; +} + +// ── Command: burn (async) ────────────────────────────────────── + +static int handleBurn(const BurnCliConfig &config, QCoreApplication &app) +{ + auto startOp = [&config](DOpticalDiscManager &manager) -> bool { + for (const auto &file : config.stageFiles) { + if (!manager.setStageFile(file)) { + cerr << "Error: Could not add '" << file.toStdString() << "' to burn queue." << endl; + cerr << " " << manager.lastError().toStdString() << endl; + return false; + } + } + + cout << "Device: " << config.device.toStdString() << endl; + cout << "Volume: " << config.volumeId.toStdString() << endl; + cout << "Mode: " << burnOptionsSummary(config.burnOptions).toStdString() << endl; + cout << "Files: " << config.stageFiles.join(", ").toStdString() << endl; + cout << endl; + + if (!manager.commit(config.burnOptions, config.speed, config.volumeId)) { + cerr << "Error: Failed to start burn operation." << endl; + cerr << " " << manager.lastError().toStdString() << endl; + return false; + } + return true; + }; + + return runAsyncJob(config, app, startOp, "Burning"); +} + +// ── Command: write-iso (async) ───────────────────────────────── + +static int handleWriteISO(const BurnCliConfig &config, QCoreApplication &app) +{ + QFileInfo isoInfo(config.isoPath); + cout << "Device: " << config.device.toStdString() << endl; + cout << "ISO: " << config.isoPath.toStdString() << endl; + cout << "Size: " << formatSize(isoInfo.size()).toStdString() << endl; + cout << endl; + + auto startOp = [&config](DOpticalDiscManager &manager) -> bool { + if (!manager.writeISO(config.isoPath, config.speed)) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return false; + } + return true; + }; + + return runAsyncJob(config, app, startOp, "Writing ISO"); +} + +// ── Command: dump-iso (async) ────────────────────────────────── + +static int handleDumpISO(const BurnCliConfig &config, QCoreApplication &app) +{ + cout << "Device: " << config.device.toStdString() << endl; + cout << "Output: " << config.outputPath.toStdString() << endl; + cout << endl; + + auto startOp = [&config](DOpticalDiscManager &manager) -> bool { + if (!manager.dumpISO(config.outputPath)) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return false; + } + return true; + }; + + return runAsyncJob(config, app, startOp, "Dumping"); +} + +// ── Command: erase (async) ───────────────────────────────────── + +static int handleErase(const BurnCliConfig &config, QCoreApplication &app) +{ + cout << "Device: " << config.device.toStdString() << endl; + cout << endl; + + auto startOp = [](DOpticalDiscManager &manager) -> bool { + if (!manager.erase()) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return false; + } + return true; + }; + + return runAsyncJob(config, app, startOp, "Erasing"); +} + +// ── Command: check (async, custom handler) ───────────────────── + +static int handleCheck(const BurnCliConfig &config, QCoreApplication &app) +{ + DOpticalDiscManager manager(config.device); + int exitCode = 1; + double good = 0, slow = 0, bad = 0; + + QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, + [&exitCode, &app, &config, &good, &slow, &bad]( + JobStatus status, int progress, + const QString &speed, const QStringList &message) { + if (status == JobStatus::kRunning) { + cout << "\rChecking... " << progress << "%" << flush; + } else if (status == JobStatus::kFinished) { + cout << endl; + bool passed = !(bad > (2 + 1e-6)); + + if (config.jsonOutput) { + QJsonObject obj; + obj["good"] = good; + obj["slow"] = slow; + obj["bad"] = bad; + obj["passed"] = passed; + cout << QJsonDocument(obj).toJson(QJsonDocument::Indented).toStdString(); + } else { + cout << fixed << setprecision(1); + cout << "Quality:" << endl; + cout << " Good: " << good << "%" << endl; + cout << " Slow: " << slow << "%" << endl; + cout << " Bad: " << bad << "%" << endl; + cout << endl; + cout << "Result: " << (passed ? "PASS" : "FAIL") << endl; + } + + exitCode = passed ? 0 : 1; + app.quit(); + } else if (status == JobStatus::kFailed) { + cerr << endl + << "Check failed: " << message.join(" ").toStdString() << endl; + exitCode = 1; + app.quit(); + } + }); + + if (!manager.checkmedia(&good, &slow, &bad)) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return 1; + } + + return app.exec(); +} + +// ── Packet Writing Commands (sync) ───────────────────────────── + +static int handlePwOpen(const BurnCliConfig &config) +{ + DPacketWritingController controller(config.device, config.workingPath); + + if (!controller.open()) { + cerr << "Error: Failed to open packet writing session." << endl; + cerr << " " << controller.lastError().toStdString() << endl; + return 1; + } + + cout << "Packet writing session opened." << endl; + cout << " Device: " << config.device.toStdString() << endl; + cout << " Working: " << config.workingPath.toStdString() << endl; + return 0; +} + +static int handlePwClose(const BurnCliConfig &config) +{ + DPacketWritingController controller(config.device, config.workingPath); + controller.close(); + + cout << "Packet writing session closed." << endl; + return 0; +} + +static int handlePwPut(const BurnCliConfig &config) +{ + DPacketWritingController controller(config.device, config.workingPath); + + if (!controller.put(config.pwFileName)) { + cerr << "Error: Failed to add file." << endl; + cerr << " " << controller.lastError().toStdString() << endl; + return 1; + } + + cout << "Added: " << config.pwFileName.toStdString() << endl; + return 0; +} + +static int handlePwMv(const BurnCliConfig &config) +{ + DPacketWritingController controller(config.device, config.workingPath); + + if (!controller.mv(config.pwSrcName, config.pwDestName)) { + cerr << "Error: Failed to rename." << endl; + cerr << " " << controller.lastError().toStdString() << endl; + return 1; + } + + cout << "Renamed: " << config.pwSrcName.toStdString() + << " -> " << config.pwDestName.toStdString() << endl; + return 0; +} + +static int handlePwRm(const BurnCliConfig &config) +{ + DPacketWritingController controller(config.device, config.workingPath); + + if (!controller.rm(config.pwFileName)) { + cerr << "Error: Failed to remove." << endl; + cerr << " " << controller.lastError().toStdString() << endl; + return 1; + } + + cout << "Removed: " << config.pwFileName.toStdString() << endl; + return 0; +} + +// ── Entry point ──────────────────────────────────────────────── int main(int argc, char *argv[]) { - QCoreApplication a(argc, argv); - // showInfo("/dev/sr0"); - // erase("/dev/sr0"); - // writeISO(); - // commit(); - // commitUDF(); - // check(); - // dumpISO(); - // pw(); - return a.exec(); + QCoreApplication app(argc, argv); + app.setApplicationName("dfm-burner"); + app.setApplicationVersion("1.0.0"); + + CliOptions cli; + BurnCliConfig config; + if (!cli.parse(argc, argv, config)) + return 1; + + switch (config.command) { + case BurnCommand::Info: + return handleInfo(config); + case BurnCommand::Burn: + return handleBurn(config, app); + case BurnCommand::WriteISO: + return handleWriteISO(config, app); + case BurnCommand::DumpISO: + return handleDumpISO(config, app); + case BurnCommand::Erase: + return handleErase(config, app); + case BurnCommand::Check: + return handleCheck(config, app); + case BurnCommand::PwOpen: + return handlePwOpen(config); + case BurnCommand::PwClose: + return handlePwClose(config); + case BurnCommand::PwPut: + return handlePwPut(config); + case BurnCommand::PwMv: + return handlePwMv(config); + case BurnCommand::PwRm: + return handlePwRm(config); + default: + return 1; + } } From e4de15d6086c514a4d2e593f089042bacbbdc740 Mon Sep 17 00:00:00 2001 From: Zhang Sheng Date: Wed, 6 May 2026 16:41:14 +0800 Subject: [PATCH 2/4] feat: add SM3 checksum verification for disc burning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Added SM3 checksum generation and verification capabilities 2. Implemented new manifest system to store file hashes before burning 3. Added new CLI commands for checksum generation and verification 4. Added libssl-dev as new dependency for cryptographic functions 5. Integrated checksum verification into optical disc manager 6. Added cache management for temporary extraction during verification Log: 1. Added 'checksum gen' command to generate SM3 manifest 2. Added 'checksum verify' command to validate disc integrity 3. Enhanced error reporting for checksum mismatches Influence: 1. Test checksum generation on various file types and sizes 2. Verify checksum validation with correct and corrupted files 3. Test manifest generation with different ISO base paths 4. Verify cache cleanup after verification completes 5. Test command line help and error messages for new features 6. Validate performance impact with large directories 7. Test cross-platform compatibility of manifest files feat: 新增光盘刻录的SM3校验功能 1. 新增SM3校验生成和验证功能 2. 实现新的校验清单系统用于存储刻录前的文件哈希值 3. 新增命令行指令用于校验生成和验证 4. 添加libssl-dev作为新的加密功能依赖项 5. 将校验验证功能集成到光盘管理器中 6. 为验证过程中的临时解压添加缓存管理 Log: 1. 新增'checksum gen'命令用于生成SM3清单 2. 新增'checksum verify'命令用于验证光盘完整性 3. 增强校验不匹配时的错误报告 Influence: 1. 测试对不同类型和大小的文件生成校验和 2. 验证对正确和已损坏文件的校验过程 3. 使用不同ISO基础路径测试清单生成 4. 验证完成后确认缓存清理正常 5. 测试新功能的命令行帮助和错误消息 6. 验证对大目录的性能影响 7. 测试清单文件的跨平台兼容性 Task: https://pms.uniontech.com/task-view-388937.html --- debian/control | 1 + debian/control.in | 1 + include/dfm-burn/dfm-burn/dburn_global.h | 1 + .../dfm-burn/dfm-burn/dopticaldiscmanager.h | 2 + src/dfm-burn/dfm-burn-client/CMakeLists.txt | 4 - src/dfm-burn/dfm-burn-client/cli_options.cpp | 140 ++++++++++++ src/dfm-burn/dfm-burn-client/cli_options.h | 7 + src/dfm-burn/dfm-burn-client/main.cpp | 63 ++++++ src/dfm-burn/dfm-burn-lib/dfm-burn.cmake | 2 + .../dfm-burn-lib/dopticaldiscmanager.cpp | 199 ++++++++++++++++++ .../dfm-burn-lib/private/dsm3hash.cpp | 64 ++++++ src/dfm-burn/dfm-burn-lib/private/dsm3hash.h | 27 +++ .../dfm-burn-lib/private/dxorrisoengine.cpp | 27 +++ .../dfm-burn-lib/private/dxorrisoengine.h | 1 + 14 files changed, 535 insertions(+), 4 deletions(-) create mode 100644 src/dfm-burn/dfm-burn-lib/private/dsm3hash.cpp create mode 100644 src/dfm-burn/dfm-burn-lib/private/dsm3hash.h diff --git a/debian/control b/debian/control index 578fd30c..866047ad 100644 --- a/debian/control +++ b/debian/control @@ -23,6 +23,7 @@ Build-Depends: libglib2.0-dev, libudisks2-dev, libisoburn-dev, + libssl-dev, libmediainfo-dev, libsecret-1-dev, liblucene++-dev, diff --git a/debian/control.in b/debian/control.in index 78486393..6335b292 100644 --- a/debian/control.in +++ b/debian/control.in @@ -14,6 +14,7 @@ Build-Depends: libglib2.0-dev, libudisks2-dev, libisoburn-dev, + libssl-dev, libmediainfo-dev, libsecret-1-dev, liblucene++-dev, diff --git a/include/dfm-burn/dfm-burn/dburn_global.h b/include/dfm-burn/dfm-burn/dburn_global.h index 674650fa..4a7fdec9 100644 --- a/include/dfm-burn/dfm-burn/dburn_global.h +++ b/include/dfm-burn/dfm-burn/dburn_global.h @@ -22,6 +22,7 @@ enum class BurnOption : unsigned int { kJolietSupport = 1 << 4, // add joliet extension kRockRidgeSupport = 1 << 5, // add rockridge extension kUDF102Supported = 1 << 6, + kChecksum = 1 << 7, // SM3 checksum verification kJolietAndRockRidge = kJolietSupport | kRockRidgeSupport // add both of them, not used yet }; Q_DECLARE_FLAGS(BurnOptions, BurnOption) diff --git a/include/dfm-burn/dfm-burn/dopticaldiscmanager.h b/include/dfm-burn/dfm-burn/dopticaldiscmanager.h index d092d0fd..78c74c4c 100644 --- a/include/dfm-burn/dfm-burn/dopticaldiscmanager.h +++ b/include/dfm-burn/dfm-burn/dopticaldiscmanager.h @@ -33,6 +33,8 @@ class DOpticalDiscManager : public QObject bool checkmedia(double *qgood, double *qslow, double *qbad); bool writeISO(const QString &isoPath, int speed = 0); bool dumpISO(const QString &isoPath); + bool generateChecksumManifest(const QString &savePath); + bool verifyChecksum(const QString &manifestPath); QString lastError() const; Q_SIGNALS: diff --git a/src/dfm-burn/dfm-burn-client/CMakeLists.txt b/src/dfm-burn/dfm-burn-client/CMakeLists.txt index 242e4a52..67f027c7 100644 --- a/src/dfm-burn/dfm-burn-client/CMakeLists.txt +++ b/src/dfm-burn/dfm-burn-client/CMakeLists.txt @@ -14,10 +14,6 @@ find_package(Qt${QT_VERSION_MAJOR}Core REQUIRED) add_executable(${PROJECT_NAME} ${SRCS}) -include_directories( - ${PROJECT_SOURCE_DIR}/../../include -) - target_link_libraries( ${PROJECT_NAME} Qt${QT_VERSION_MAJOR}::Core diff --git a/src/dfm-burn/dfm-burn-client/cli_options.cpp b/src/dfm-burn/dfm-burn-client/cli_options.cpp index a20690dd..b2a8edfb 100644 --- a/src/dfm-burn/dfm-burn-client/cli_options.cpp +++ b/src/dfm-burn/dfm-burn-client/cli_options.cpp @@ -45,6 +45,25 @@ bool CliOptions::parse(int argc, char *argv[], BurnCliConfig &config) return parseEraseArgs(args, config); if (cmd == "check") return parseCheckArgs(args, config); + if (cmd == "checksum") { + if (args.isEmpty()) { + cerr << "Error: 'checksum' requires a subcommand (gen, verify)" << endl; + cerr << "Run 'dfm-burner checksum --help' for details." << endl; + return false; + } + QString checksumAction = args.takeFirst(); + if (checksumAction == "gen") + return parseChecksumGenArgs(args, config); + if (checksumAction == "verify") + return parseChecksumVerifyArgs(args, config); + if (checksumAction == "--help" || checksumAction == "-h") { + printCommandHelp(BurnCommand::ChecksumGen); + return false; + } + cerr << "Error: Unknown checksum action '" << checksumAction.toStdString() << "'" << endl; + cerr << "Valid actions: gen, verify" << endl; + return false; + } if (cmd == "pw") { if (args.isEmpty()) { cerr << "Error: 'pw' requires a subcommand (open, close, put, mv, rm)" << endl; @@ -76,6 +95,7 @@ void CliOptions::printHelp() const cout << " dump-iso Read disc and save as .iso image" << endl; cout << " erase Erase a rewritable disc (CD-RW, DVD-RW, BD-RE)" << endl; cout << " check Scan disc for read errors" << endl; + cout << " checksum SM3 checksum verification (gen / verify)" << endl; cout << " pw Incremental file operations (UDF packet writing)" << endl; cout << endl; cout << "Tips:" << endl; @@ -214,6 +234,38 @@ void CliOptions::printCommandHelp(BurnCommand cmd) const cout << " dfm-burner check --json /dev/sr0" << endl; break; + case BurnCommand::ChecksumGen: + cout << "Usage: dfm-burner checksum gen --output= " << endl; + cout << endl; + cout << "Generate an SM3 checksum manifest for files before burning." << endl; + cout << "The manifest records the SM3 hash of every file, which can later be" << endl; + cout << "used to verify disc integrity with 'dfm-burner checksum verify'." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " source_dir Directory containing files to be burned." << endl; + cout << endl; + cout << "Options:" << endl; + cout << " --output= Where to save the manifest JSON (required)." << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner checksum gen --output=manifest.json ./my-data" << endl; + break; + + case BurnCommand::ChecksumVerify: + cout << "Usage: dfm-burner checksum verify " << endl; + cout << endl; + cout << "Verify burned disc integrity by comparing SM3 checksums." << endl; + cout << "Extracts all files from the disc and compares their SM3 hashes" << endl; + cout << "against the manifest generated by 'dfm-burner checksum gen'." << endl; + cout << endl; + cout << "Arguments:" << endl; + cout << " device Device path (e.g., /dev/sr0). Disc must still be in drive." << endl; + cout << " manifest.json Path to the manifest file created during 'gen'." << endl; + cout << endl; + cout << "Examples:" << endl; + cout << " dfm-burner checksum verify /dev/sr0 manifest.json" << endl; + break; + case BurnCommand::PwOpen: case BurnCommand::PwClose: case BurnCommand::PwPut: @@ -481,6 +533,94 @@ bool CliOptions::parseCheckArgs(const QStringList &args, BurnCliConfig &config) return true; } +bool CliOptions::parseChecksumGenArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::ChecksumGen; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::ChecksumGen); + return false; + } + if (arg.startsWith("--output=")) { + config.manifestPath = arg.mid(9); + if (config.manifestPath.isEmpty()) { + cerr << "Error: --output requires a non-empty value." << endl; + return false; + } + } else if (!arg.startsWith('-')) { + if (config.stageFiles.isEmpty()) + config.stageFiles << arg; + else { + cerr << "Error: Unexpected argument '" << arg.toStdString() << "'" << endl; + return false; + } + } else { + cerr << "Error: Unknown option '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.manifestPath.isEmpty()) { + cerr << "Error: --output= is required." << endl; + cerr << "Run 'dfm-burner checksum gen --help' for usage." << endl; + return false; + } + if (config.stageFiles.isEmpty()) { + cerr << "Error: Source directory is required." << endl; + cerr << "Run 'dfm-burner checksum gen --help' for usage." << endl; + return false; + } + if (!QFileInfo::exists(config.stageFiles.first())) { + cerr << "Error: Source not found: " << config.stageFiles.first().toStdString() << endl; + return false; + } + + return true; +} + +bool CliOptions::parseChecksumVerifyArgs(const QStringList &args, BurnCliConfig &config) const +{ + config.command = BurnCommand::ChecksumVerify; + + for (const auto &arg : args) { + if (arg == "--help" || arg == "-h") { + printCommandHelp(BurnCommand::ChecksumVerify); + return false; + } + if (!arg.startsWith('-')) { + if (config.device.isEmpty()) + config.device = arg; + else if (config.manifestPath.isEmpty()) + config.manifestPath = arg; + else { + cerr << "Error: Unexpected argument '" << arg.toStdString() << "'" << endl; + return false; + } + } else { + cerr << "Error: Unknown option '" << arg.toStdString() << "'" << endl; + return false; + } + } + + if (config.device.isEmpty()) { + cerr << "Error: Device path is required." << endl; + cerr << "Run 'dfm-burner checksum verify --help' for usage." << endl; + return false; + } + if (config.manifestPath.isEmpty()) { + cerr << "Error: Manifest path is required." << endl; + cerr << "Run 'dfm-burner checksum verify --help' for usage." << endl; + return false; + } + if (!QFileInfo::exists(config.manifestPath)) { + cerr << "Error: Manifest not found: " << config.manifestPath.toStdString() << endl; + return false; + } + + return true; +} + bool CliOptions::parsePwArgs(const QString &pwAction, const QStringList &args, BurnCliConfig &config) const { // Map action to command diff --git a/src/dfm-burn/dfm-burn-client/cli_options.h b/src/dfm-burn/dfm-burn-client/cli_options.h index 14a86c3f..8d34b7d4 100644 --- a/src/dfm-burn/dfm-burn-client/cli_options.h +++ b/src/dfm-burn/dfm-burn-client/cli_options.h @@ -21,6 +21,8 @@ enum class BurnCommand { DumpISO, Erase, Check, + ChecksumGen, + ChecksumVerify, PwOpen, PwClose, PwPut, @@ -55,6 +57,9 @@ struct BurnCliConfig QString pwSrcName; QString pwDestName; QString pwFileName; + + // checksum + QString manifestPath; }; /** @@ -87,6 +92,8 @@ class CliOptions bool parseDumpIsoArgs(const QStringList &args, BurnCliConfig &config) const; bool parseEraseArgs(const QStringList &args, BurnCliConfig &config) const; bool parseCheckArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseChecksumGenArgs(const QStringList &args, BurnCliConfig &config) const; + bool parseChecksumVerifyArgs(const QStringList &args, BurnCliConfig &config) const; bool parsePwArgs(const QString &pwAction, const QStringList &args, BurnCliConfig &config) const; }; diff --git a/src/dfm-burn/dfm-burn-client/main.cpp b/src/dfm-burn/dfm-burn-client/main.cpp index 185e2eb9..bdf0187d 100644 --- a/src/dfm-burn/dfm-burn-client/main.cpp +++ b/src/dfm-burn/dfm-burn-client/main.cpp @@ -271,6 +271,65 @@ static int handleCheck(const BurnCliConfig &config, QCoreApplication &app) return app.exec(); } +// ── Command: checksum gen (sync) ──────────────────────────────── + +static int handleChecksumGen(const BurnCliConfig &config) +{ + DOpticalDiscManager manager(config.device); + + if (!manager.setStageFile(config.stageFiles.first())) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return 1; + } + + if (!manager.generateChecksumManifest(config.manifestPath)) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return 1; + } + + cout << "Manifest generated: " << config.manifestPath.toStdString() << endl; + return 0; +} + +// ── Command: checksum verify (async) ───────────────────────────── + +static int handleChecksumVerify(const BurnCliConfig &config, QCoreApplication &app) +{ + DOpticalDiscManager manager(config.device); + int exitCode = 1; + + QObject::connect(&manager, &DOpticalDiscManager::jobStatusChanged, + [&exitCode, &app](JobStatus status, int progress, + const QString &speed, const QStringList &message) { + Q_UNUSED(speed); + if (status == JobStatus::kRunning) { + cout << "\rVerifying... " << progress << "%"; + if (!message.isEmpty()) + cout << " (" << message.first().toStdString() << ")"; + cout << flush; + } else if (status == JobStatus::kStalled) { + cout << "\rVerifying... Stalled (waiting for drive)." << flush; + } else if (status == JobStatus::kFinished) { + cout << "\rVerifying... Done. " << endl; + cout << "All checksums matched." << endl; + exitCode = 0; + app.quit(); + } else if (status == JobStatus::kFailed) { + cerr << endl + << "Verification failed: " << message.join(" ").toStdString() << endl; + exitCode = 1; + app.quit(); + } + }); + + if (!manager.verifyChecksum(config.manifestPath)) { + cerr << "Error: " << manager.lastError().toStdString() << endl; + return 1; + } + + return app.exec(); +} + // ── Packet Writing Commands (sync) ───────────────────────────── static int handlePwOpen(const BurnCliConfig &config) @@ -367,6 +426,10 @@ int main(int argc, char *argv[]) return handleErase(config, app); case BurnCommand::Check: return handleCheck(config, app); + case BurnCommand::ChecksumGen: + return handleChecksumGen(config); + case BurnCommand::ChecksumVerify: + return handleChecksumVerify(config, app); case BurnCommand::PwOpen: return handlePwOpen(config); case BurnCommand::PwClose: diff --git a/src/dfm-burn/dfm-burn-lib/dfm-burn.cmake b/src/dfm-burn/dfm-burn-lib/dfm-burn.cmake index 58a69d9b..c6d5d576 100644 --- a/src/dfm-burn/dfm-burn-lib/dfm-burn.cmake +++ b/src/dfm-burn/dfm-burn-lib/dfm-burn.cmake @@ -1,6 +1,7 @@ # Setup the environment find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED) find_package(PkgConfig REQUIRED) +find_package(OpenSSL REQUIRED COMPONENTS Crypto) pkg_check_modules(isoburn REQUIRED libisoburn-1 IMPORTED_TARGET) # Build @@ -13,6 +14,7 @@ add_library(${BIN_NAME} SHARED target_link_libraries(${BIN_NAME} Qt${QT_VERSION_MAJOR}::Core PkgConfig::isoburn + OpenSSL::Crypto ) target_include_directories( diff --git a/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp b/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp index ffef12b8..1c69caea 100644 --- a/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp +++ b/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp @@ -8,8 +8,15 @@ #include "private/dopticaldiscmanager_p.h" #include "private/dxorrisoengine.h" #include "private/dudfburnengine.h" +#include "private/dsm3hash.h" #include +#include +#include +#include +#include +#include +#include #include #include @@ -212,6 +219,198 @@ bool DOpticalDiscManager::dumpISO(const QString &isoPath) return ret; } +static const QString kExtractCacheSubDir = "discburn"; + +static QString extractCacheDir(const QString &dev) +{ + QString safeDev = dev; + safeDev.replace("/", "_"); + // Remove leading underscore caused by the leading "/" in device path + if (safeDev.startsWith("_")) + safeDev.remove(0, 1); + return QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + + "/" + kExtractCacheSubDir + "/extract_" + safeDev; +} + +// xorriso extracts files with read-only permissions (dr-xr-xr-x), +// so we must chmod recursively before removal. +static void cleanupExtractCache(QDir dir) +{ + if (!dir.exists()) + return; + QDirIterator it(dir.absolutePath(), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, + QDirIterator::Subdirectories); + while (it.hasNext()) { + QString path = it.next(); + QFile::setPermissions(path, QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner + | QFile::ReadGroup | QFile::ExeGroup + | QFile::ReadOther | QFile::ExeOther); + } + dir.removeRecursively(); +} + +bool DOpticalDiscManager::generateChecksumManifest(const QString &savePath) +{ + QString srcPath = dptr->files.first; + if (srcPath.isEmpty()) { + dptr->errorMsg = "No staged file for checksum generation"; + return false; + } + + QFileInfo srcInfo(srcPath); + if (!srcInfo.exists()) { + dptr->errorMsg = QString("Source path does not exist: %1").arg(srcPath); + return false; + } + + QString isoBase = dptr->files.second; + if (isoBase.isEmpty()) + isoBase = "/"; + + // Normalize isoBase: ensure it ends with / + if (!isoBase.endsWith("/")) + isoBase += "/"; + + QJsonObject filesObj; + + if (srcInfo.isDir()) { + QDirIterator it(srcPath, QDir::Files, QDirIterator::Subdirectories); + while (it.hasNext()) { + QString filePath = it.next(); + QString relPath = QDir(srcPath).relativeFilePath(filePath); + QString isoRelPath = isoBase + relPath; + + QString hash = DSM3Hash::sm3File(filePath); + if (hash.isEmpty()) { + dptr->errorMsg = QString("Failed to compute SM3 for: %1").arg(filePath); + return false; + } + filesObj[isoRelPath] = hash; + } + } else { + QString hash = DSM3Hash::sm3File(srcPath); + if (hash.isEmpty()) { + dptr->errorMsg = QString("Failed to compute SM3 for: %1").arg(srcPath); + return false; + } + filesObj[isoBase + srcInfo.fileName()] = hash; + } + + if (filesObj.isEmpty()) { + dptr->errorMsg = "No files found to checksum"; + return false; + } + + QJsonObject root; + root["algorithm"] = "sm3"; + root["device"] = dptr->curDev; + root["files"] = filesObj; + + QFile outFile(savePath); + if (!outFile.open(QIODevice::WriteOnly)) { + dptr->errorMsg = QString("Cannot write manifest to: %1").arg(savePath); + return false; + } + + outFile.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); + return true; +} + +bool DOpticalDiscManager::verifyChecksum(const QString &manifestPath) +{ + QFile mf(manifestPath); + if (!mf.open(QIODevice::ReadOnly)) { + dptr->errorMsg = QString("Cannot read manifest: %1").arg(manifestPath); + return false; + } + + QJsonParseError parseErr; + QJsonObject root = QJsonDocument::fromJson(mf.readAll(), &parseErr).object(); + if (root.isEmpty()) { + dptr->errorMsg = QString("Invalid manifest JSON: %1").arg(parseErr.errorString()); + return false; + } + + QJsonObject filesObj = root["files"].toObject(); + if (filesObj.isEmpty()) { + dptr->errorMsg = "Manifest contains no file entries"; + return false; + } + + // Prepare cache directory for extraction + QString cacheBase = extractCacheDir(dptr->curDev); + QDir cacheDir(cacheBase); + if (cacheDir.exists()) + cleanupExtractCache(cacheDir); + if (!cacheDir.mkpath(".")) { + dptr->errorMsg = QString("Cannot create cache directory: %1").arg(cacheBase); + return false; + } + + // Extract entire disc tree via xorriso osirrox + QScopedPointer engine { new DXorrisoEngine }; + connect(engine.data(), &DXorrisoEngine::jobStatusChanged, this, + [this, ptr = QPointer(engine.data())](JobStatus status, int progress, QString speed) { + if (ptr) + Q_EMIT jobStatusChanged(status, progress, speed, ptr->takeInfoMessages()); + }, + Qt::DirectConnection); + + if (!engine->acquireDevice(dptr->curDev)) { + dptr->errorMsg = "Cannot acquire device for verification"; + cleanupExtractCache(cacheDir); + return false; + } + + if (!engine->doExtract(cacheBase, "/")) { + dptr->errorMsg = QString("Failed to extract disc content: %1").arg(engine->takeInfoMessages().join(" ")); + engine->releaseDevice(); + cleanupExtractCache(cacheDir); + return false; + } + + engine->releaseDevice(); + + // Verify each file in manifest against extracted files + QStringList isoPaths = filesObj.keys(); + int total = isoPaths.size(); + int checked = 0; + + for (const auto &isoPath : isoPaths) { + // Map ISO path to local extracted path + // ISO path is like "/file.txt" or "/dir/file.txt" + // Extracted to cacheBase + isoPath + QString localExtracted = cacheBase + isoPath; + + QString expectedHash = filesObj[isoPath].toString(); + QString actualHash = DSM3Hash::sm3File(localExtracted); + + ++checked; + int pct = (total > 0) ? (100 * checked / total) : 0; + Q_EMIT jobStatusChanged(JobStatus::kRunning, pct, {}, { isoPath }); + + if (actualHash.isEmpty()) { + dptr->errorMsg = QString("Checksum mismatch: file not found on disc: %1").arg(isoPath); + cleanupExtractCache(cacheDir); + return false; + } + + if (actualHash != expectedHash) { + dptr->errorMsg = QString("Checksum mismatch: %1 (expected %2, got %3)") + .arg(isoPath, expectedHash, actualHash); + cleanupExtractCache(cacheDir); + return false; + } + } + + Q_EMIT jobStatusChanged(JobStatus::kFinished, 100, {}, {}); + + // Clean up extracted files + cleanupExtractCache(cacheDir); + + return true; +} + QString DOpticalDiscManager::lastError() const { return dptr->errorMsg; diff --git a/src/dfm-burn/dfm-burn-lib/private/dsm3hash.cpp b/src/dfm-burn/dfm-burn-lib/private/dsm3hash.cpp new file mode 100644 index 00000000..15eba643 --- /dev/null +++ b/src/dfm-burn/dfm-burn-lib/private/dsm3hash.cpp @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "dsm3hash.h" + +#include + +#include + +DFM_BURN_USE_NS + +static constexpr int kSM3HashLen = 32; // SM3 produces 256-bit (32-byte) hash +static constexpr int kBufferSize = 8192; + +static QString toHex(const unsigned char *data, int len) +{ + QString result; + result.reserve(len * 2); + static const char kHex[] = "0123456789abcdef"; + for (int i = 0; i < len; ++i) { + result.append(kHex[(data[i] >> 4) & 0x0F]); + result.append(kHex[data[i] & 0x0F]); + } + return result; +} + +QString DSM3Hash::sm3File(const QString &filePath) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) + return {}; + + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + if (!ctx) + return {}; + + const EVP_MD *md = EVP_sm3(); + if (!md || EVP_DigestInit_ex(ctx, md, nullptr) != 1) { + EVP_MD_CTX_free(ctx); + return {}; + } + + char buf[kBufferSize]; + while (!file.atEnd()) { + qint64 n = file.read(buf, kBufferSize); + if (n <= 0) + break; + if (EVP_DigestUpdate(ctx, buf, static_cast(n)) != 1) { + EVP_MD_CTX_free(ctx); + return {}; + } + } + + unsigned char hash[kSM3HashLen]; + unsigned int hashLen = 0; + if (EVP_DigestFinal_ex(ctx, hash, &hashLen) != 1) { + EVP_MD_CTX_free(ctx); + return {}; + } + + EVP_MD_CTX_free(ctx); + return toHex(hash, hashLen); +} diff --git a/src/dfm-burn/dfm-burn-lib/private/dsm3hash.h b/src/dfm-burn/dfm-burn-lib/private/dsm3hash.h new file mode 100644 index 00000000..8c57b996 --- /dev/null +++ b/src/dfm-burn/dfm-burn-lib/private/dsm3hash.h @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef DSM3HASH_H +#define DSM3HASH_H + +#include + +#include + +DFM_BURN_BEGIN_NS + +class DSM3Hash +{ +public: + /*! + * \brief Compute SM3 hash of a file. + * \param filePath Absolute path to the file. + * \return Hex-encoded SM3 hash string (64 chars), or empty string on failure. + */ + static QString sm3File(const QString &filePath); +}; + +DFM_BURN_END_NS + +#endif // DSM3HASH_H diff --git a/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.cpp b/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.cpp index ae2bd9c2..b1199425 100644 --- a/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.cpp +++ b/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.cpp @@ -515,6 +515,33 @@ bool DXorrisoEngine::doBurn(const QPair files, int speed, QStr return true; } +bool DXorrisoEngine::doExtract(const QString &diskPath, const QString &isoPath) +{ + Q_EMIT jobStatusChanged(JobStatus::kStalled, 0, curspeed); + xorrisomsg.clear(); + + // Enable osirrox mode: allows copying from ISO filesystem to disk + int r = XORRISO_OPT(xorriso, [this]() { + return Xorriso_option_osirrox(xorriso, PCHAR("on"), 0); + }); + if (JOBFAILED_IF(this, r, xorriso)) + return false; + + // Extract files from disc to local disk path + // NOTE: Despite the API parameter naming (disk_path, iso_path), xorriso actually + // treats the first argument as the ISO source path and the second as disk destination. + // This matches the CLI behavior: -extract + r = XORRISO_OPT(xorriso, [this, diskPath, isoPath]() { + return Xorriso_option_extract(xorriso, + PCHAR(isoPath.toUtf8().data()), + PCHAR(diskPath.toUtf8().data()), 0); + }); + if (JOBFAILED_IF(this, r, xorriso)) + return false; + + return true; +} + void DXorrisoEngine::messageReceived(int type, char *text) { Q_UNUSED(type); diff --git a/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.h b/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.h index 570b7e69..bcfa2a9d 100644 --- a/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.h +++ b/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.h @@ -53,6 +53,7 @@ class DXorrisoEngine : public QObject bool doCheckmedia(quint64 dataBlocks, double *qgood, double *qslow, double *qbad); bool doBurn(const QPair files, int speed, QString volId, JolietSupport joliet, RockRageSupport rockRage, KeepAppendable appendable); + bool doExtract(const QString &diskPath, const QString &isoPath); public Q_SLOTS: void messageReceived(int type, char *text); From ce2b7a34c89148def41201677d5360cd08a49e57 Mon Sep 17 00:00:00 2001 From: Zhang Sheng Date: Thu, 7 May 2026 09:28:35 +0800 Subject: [PATCH 3/4] perf: optimize disc checksum verification process MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Changed verification to extract and check files individually instead of extracting entire disc 2. Added explicit osirrox enable/disable control 3. Improved memory efficiency by deleting processed files immediately 4. Better error handling with proper resource cleanup 5. Made process more reliable for multi-session discs The previous implementation would extract the entire disc contents to verify checksums, which was inefficient and problematic for multi- session discs. The new approach extracts files one at a time, computes their hash, then removes them immediately. This improves memory usage and compatibility with different disc types. Log: Improved disc verification reliability and performance Influence: 1. Test verification with normal single-session discs 2. Test verification with multi-session discs 3. Verify behavior when checksums match and when they don't 4. Check memory usage during verification 5. Test with various file types and sizes 6. Verify cleanup of temporary files after completion perf: 优化光盘校验过程 1. 改为逐个提取并校验文件,而不是提取整个光盘内容 2. 添加显式的osirrox启用/禁用控制 3. 通过立即删除已处理文件提高内存效率 4. 改进错误处理,确保正确释放资源 5. 提升对多区段光盘的可靠性 之前的实现会提取整个光盘内容来校验哈希值,效率低下且对多区段光盘有问题。 新方法逐个提取文件,计算哈希后立即删除,提高了内存使用效率和不同光盘类型 的兼容性。 Log: 提高了光盘校验的可靠性和性能 Influence: 1. 测试普通单区段光盘的验证 2. 测试多区段光盘的验证 3. 验证哈希值匹配和不匹配时的行为 4. 检查验证过程中的内存使用情况 5. 测试不同类型和大小的文件 6. 验证完成后临时文件的清理情况 --- .../dfm-burn-lib/dopticaldiscmanager.cpp | 42 +++++++++++++------ .../dfm-burn-lib/private/dxorrisoengine.cpp | 17 ++++---- .../dfm-burn-lib/private/dxorrisoengine.h | 1 + 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp b/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp index 1c69caea..d2fd7921 100644 --- a/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp +++ b/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp @@ -337,7 +337,7 @@ bool DOpticalDiscManager::verifyChecksum(const QString &manifestPath) return false; } - // Prepare cache directory for extraction + // Prepare temp directory for per-file extraction QString cacheBase = extractCacheDir(dptr->curDev); QDir cacheDir(cacheBase); if (cacheDir.exists()) @@ -347,7 +347,7 @@ bool DOpticalDiscManager::verifyChecksum(const QString &manifestPath) return false; } - // Extract entire disc tree via xorriso osirrox + // Acquire device and enable osirrox once for all per-file extractions QScopedPointer engine { new DXorrisoEngine }; connect(engine.data(), &DXorrisoEngine::jobStatusChanged, this, [this, ptr = QPointer(engine.data())](JobStatus status, int progress, QString speed) { @@ -362,28 +362,40 @@ bool DOpticalDiscManager::verifyChecksum(const QString &manifestPath) return false; } - if (!engine->doExtract(cacheBase, "/")) { - dptr->errorMsg = QString("Failed to extract disc content: %1").arg(engine->takeInfoMessages().join(" ")); + if (!engine->doOsirroxOn()) { + dptr->errorMsg = QString("Failed to enable osirrox: %1").arg(engine->takeInfoMessages().join(" ")); engine->releaseDevice(); cleanupExtractCache(cacheDir); return false; } - engine->releaseDevice(); - - // Verify each file in manifest against extracted files + // Extract and verify each file individually: + // extract → compute SM3 → delete → next file + // This avoids extracting the entire disc (multi-session friendly) + // and minimizes disk usage (only one file at a time). QStringList isoPaths = filesObj.keys(); int total = isoPaths.size(); int checked = 0; for (const auto &isoPath : isoPaths) { - // Map ISO path to local extracted path - // ISO path is like "/file.txt" or "/dir/file.txt" - // Extracted to cacheBase + isoPath - QString localExtracted = cacheBase + isoPath; + QString localTmp = cacheBase + isoPath; + + // Ensure parent directory exists + QDir().mkpath(QFileInfo(localTmp).absolutePath()); + + if (!engine->doExtract(localTmp, isoPath)) { + dptr->errorMsg = QString("Failed to extract '%1' from disc: %2") + .arg(isoPath, engine->takeInfoMessages().join(" ")); + engine->releaseDevice(); + cleanupExtractCache(cacheDir); + return false; + } QString expectedHash = filesObj[isoPath].toString(); - QString actualHash = DSM3Hash::sm3File(localExtracted); + QString actualHash = DSM3Hash::sm3File(localTmp); + + // Delete immediately to free disk space and avoid permission issues on cleanup + QFile::remove(localTmp); ++checked; int pct = (total > 0) ? (100 * checked / total) : 0; @@ -391,6 +403,7 @@ bool DOpticalDiscManager::verifyChecksum(const QString &manifestPath) if (actualHash.isEmpty()) { dptr->errorMsg = QString("Checksum mismatch: file not found on disc: %1").arg(isoPath); + engine->releaseDevice(); cleanupExtractCache(cacheDir); return false; } @@ -398,14 +411,17 @@ bool DOpticalDiscManager::verifyChecksum(const QString &manifestPath) if (actualHash != expectedHash) { dptr->errorMsg = QString("Checksum mismatch: %1 (expected %2, got %3)") .arg(isoPath, expectedHash, actualHash); + engine->releaseDevice(); cleanupExtractCache(cacheDir); return false; } } + engine->releaseDevice(); + Q_EMIT jobStatusChanged(JobStatus::kFinished, 100, {}, {}); - // Clean up extracted files + // Clean up cache directory structure cleanupExtractCache(cacheDir); return true; diff --git a/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.cpp b/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.cpp index b1199425..e91d4c1f 100644 --- a/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.cpp +++ b/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.cpp @@ -515,23 +515,22 @@ bool DXorrisoEngine::doBurn(const QPair files, int speed, QStr return true; } -bool DXorrisoEngine::doExtract(const QString &diskPath, const QString &isoPath) +bool DXorrisoEngine::doOsirroxOn() { - Q_EMIT jobStatusChanged(JobStatus::kStalled, 0, curspeed); - xorrisomsg.clear(); - - // Enable osirrox mode: allows copying from ISO filesystem to disk int r = XORRISO_OPT(xorriso, [this]() { return Xorriso_option_osirrox(xorriso, PCHAR("on"), 0); }); - if (JOBFAILED_IF(this, r, xorriso)) - return false; + return !JOBFAILED_IF(this, r, xorriso); +} + +bool DXorrisoEngine::doExtract(const QString &diskPath, const QString &isoPath) +{ + xorrisomsg.clear(); - // Extract files from disc to local disk path // NOTE: Despite the API parameter naming (disk_path, iso_path), xorriso actually // treats the first argument as the ISO source path and the second as disk destination. // This matches the CLI behavior: -extract - r = XORRISO_OPT(xorriso, [this, diskPath, isoPath]() { + int r = XORRISO_OPT(xorriso, [this, diskPath, isoPath]() { return Xorriso_option_extract(xorriso, PCHAR(isoPath.toUtf8().data()), PCHAR(diskPath.toUtf8().data()), 0); diff --git a/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.h b/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.h index bcfa2a9d..beca39c8 100644 --- a/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.h +++ b/src/dfm-burn/dfm-burn-lib/private/dxorrisoengine.h @@ -54,6 +54,7 @@ class DXorrisoEngine : public QObject bool doBurn(const QPair files, int speed, QString volId, JolietSupport joliet, RockRageSupport rockRage, KeepAppendable appendable); bool doExtract(const QString &diskPath, const QString &isoPath); + bool doOsirroxOn(); public Q_SLOTS: void messageReceived(int type, char *text); From cd201db794ba53c9994ead26381f059494a1f9d0 Mon Sep 17 00:00:00 2001 From: Zhang Sheng Date: Thu, 7 May 2026 11:37:46 +0800 Subject: [PATCH 4/4] fix: include hidden files in checksum manifest generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modified QDirIterator flags to include hidden files when generating checksum manifests. Previously hidden files were excluded from the manifest which could cause missing files in backup scenarios where hidden files are important (like configuration files). Log: Now including hidden files in disk checksum manifests Influence: 1. Test manifest generation with directories containing hidden files 2. Verify hidden files checksums are correctly recorded in manifest 3. Check manifest verification process with hidden files included 4. Test with directories containing both regular and hidden files fix: 在生成校验和清单时包含隐藏文件 修改了 QDirIterator 的标志以在生成校验和清单时包含隐藏文件。之前版本中隐 藏文件被排除在清单之外,这在备份场景下可能导致重要隐藏文件(如配置文件) 缺失的问题。 Log: 现在校验和清单包含隐藏文件 Influence: 1. 测试包含隐藏文件的目录清单生成 2. 验证隐藏文件的校验和被正确记录到清单中 3. 检查包含隐藏文件时的清单验证过程 4. 测试同时包含常规文件和隐藏文件的目录情况 --- .../dfm-burn-lib/dopticaldiscmanager.cpp | 84 ++++++++++--------- 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp b/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp index d2fd7921..270f06de 100644 --- a/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp +++ b/src/dfm-burn/dfm-burn-lib/dopticaldiscmanager.cpp @@ -22,6 +22,37 @@ DFM_BURN_USE_NS +static const QString kExtractCacheSubDir = "discburn"; + +static QString extractCacheDir(const QString &dev) +{ + QString safeDev = dev; + safeDev.replace("/", "_"); + // Remove leading underscore caused by the leading "/" in device path + if (safeDev.startsWith("_")) + safeDev.remove(0, 1); + return QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + + "/deepin/" + kExtractCacheSubDir + "/extract_" + safeDev; +} + +// xorriso extracts files with read-only permissions (dr-xr-xr-x), +// so we must chmod recursively before removal. +static void cleanupExtractCache(QDir dir) +{ + if (!dir.exists()) + return; + QDirIterator it(dir.absolutePath(), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, + QDirIterator::Subdirectories); + while (it.hasNext()) { + QString path = it.next(); + QFile::setPermissions(path, + QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner + | QFile::ReadGroup | QFile::ExeGroup | QFile::ReadOther + | QFile::ExeOther); + } + dir.removeRecursively(); +} + DOpticalDiscManager::DOpticalDiscManager(const QString &dev, QObject *parent) : QObject(parent), dptr(new DOpticalDiscManagerPrivate) { @@ -65,7 +96,8 @@ bool DOpticalDiscManager::commit(const BurnOptions &opts, int speed, const QStri if (opts.testFlag(BurnOption::kUDF102Supported)) { QScopedPointer udfEngine { new DUDFBurnEngine }; - connect(udfEngine.data(), &DUDFBurnEngine::jobStatusChanged, this, + connect( + udfEngine.data(), &DUDFBurnEngine::jobStatusChanged, this, [this, ptr = QPointer(udfEngine.data())](JobStatus status, int progress) { if (ptr) { if (status == JobStatus::kFailed) @@ -78,7 +110,8 @@ bool DOpticalDiscManager::commit(const BurnOptions &opts, int speed, const QStri ret = udfEngine->doBurn(dptr->curDev, dptr->files, volId, opts); } else { QScopedPointer xorrisoEngine { new DXorrisoEngine }; - connect(xorrisoEngine.data(), &DXorrisoEngine::jobStatusChanged, this, + connect( + xorrisoEngine.data(), &DXorrisoEngine::jobStatusChanged, this, [this, ptr = QPointer(xorrisoEngine.data())](JobStatus status, int progress, QString speed) { if (ptr) Q_EMIT jobStatusChanged(status, progress, speed, ptr->takeInfoMessages()); @@ -112,7 +145,8 @@ bool DOpticalDiscManager::erase() { bool ret { false }; QScopedPointer engine { new DXorrisoEngine }; - connect(engine.data(), &DXorrisoEngine::jobStatusChanged, this, + connect( + engine.data(), &DXorrisoEngine::jobStatusChanged, this, [this, ptr = QPointer(engine.data())](JobStatus status, int progress, QString speed) { if (ptr) Q_EMIT jobStatusChanged(status, progress, speed, ptr->takeInfoMessages()); @@ -141,7 +175,8 @@ bool DOpticalDiscManager::checkmedia(double *qgood, double *qslow, double *qbad) } QScopedPointer engine { new DXorrisoEngine }; - connect(engine.data(), &DXorrisoEngine::jobStatusChanged, this, + connect( + engine.data(), &DXorrisoEngine::jobStatusChanged, this, [this, ptr = QPointer(engine.data())](JobStatus status, int progress, QString speed) { if (ptr) Q_EMIT jobStatusChanged(status, progress, speed, ptr->takeInfoMessages()); @@ -162,7 +197,8 @@ bool DOpticalDiscManager::writeISO(const QString &isoPath, int speed) { bool ret { false }; QScopedPointer engine { new DXorrisoEngine }; - connect(engine.data(), &DXorrisoEngine::jobStatusChanged, this, + connect( + engine.data(), &DXorrisoEngine::jobStatusChanged, this, [this, ptr = QPointer(engine.data())](JobStatus status, int progress, QString speed) { if (ptr) Q_EMIT jobStatusChanged(status, progress, speed, ptr->takeInfoMessages()); @@ -197,7 +233,8 @@ bool DOpticalDiscManager::dumpISO(const QString &isoPath) } QScopedPointer engine { new DXorrisoEngine }; - connect(engine.data(), &DXorrisoEngine::jobStatusChanged, this, + connect( + engine.data(), &DXorrisoEngine::jobStatusChanged, this, [this, ptr = QPointer(engine.data())](JobStatus status, int progress, QString speed) { if (ptr) emit jobStatusChanged(status, progress, speed, ptr->takeInfoMessages()); @@ -219,36 +256,6 @@ bool DOpticalDiscManager::dumpISO(const QString &isoPath) return ret; } -static const QString kExtractCacheSubDir = "discburn"; - -static QString extractCacheDir(const QString &dev) -{ - QString safeDev = dev; - safeDev.replace("/", "_"); - // Remove leading underscore caused by the leading "/" in device path - if (safeDev.startsWith("_")) - safeDev.remove(0, 1); - return QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) - + "/" + kExtractCacheSubDir + "/extract_" + safeDev; -} - -// xorriso extracts files with read-only permissions (dr-xr-xr-x), -// so we must chmod recursively before removal. -static void cleanupExtractCache(QDir dir) -{ - if (!dir.exists()) - return; - QDirIterator it(dir.absolutePath(), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot, - QDirIterator::Subdirectories); - while (it.hasNext()) { - QString path = it.next(); - QFile::setPermissions(path, QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner - | QFile::ReadGroup | QFile::ExeGroup - | QFile::ReadOther | QFile::ExeOther); - } - dir.removeRecursively(); -} - bool DOpticalDiscManager::generateChecksumManifest(const QString &savePath) { QString srcPath = dptr->files.first; @@ -274,7 +281,7 @@ bool DOpticalDiscManager::generateChecksumManifest(const QString &savePath) QJsonObject filesObj; if (srcInfo.isDir()) { - QDirIterator it(srcPath, QDir::Files, QDirIterator::Subdirectories); + QDirIterator it(srcPath, QDir::Files | QDir::Hidden, QDirIterator::Subdirectories); while (it.hasNext()) { QString filePath = it.next(); QString relPath = QDir(srcPath).relativeFilePath(filePath); @@ -349,7 +356,8 @@ bool DOpticalDiscManager::verifyChecksum(const QString &manifestPath) // Acquire device and enable osirrox once for all per-file extractions QScopedPointer engine { new DXorrisoEngine }; - connect(engine.data(), &DXorrisoEngine::jobStatusChanged, this, + connect( + engine.data(), &DXorrisoEngine::jobStatusChanged, this, [this, ptr = QPointer(engine.data())](JobStatus status, int progress, QString speed) { if (ptr) Q_EMIT jobStatusChanged(status, progress, speed, ptr->takeInfoMessages());