Skip to content

fix(djvu): copy network djvu files to local temp dir before loading#275

Merged
deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
LiHua000:master
May 14, 2026
Merged

fix(djvu): copy network djvu files to local temp dir before loading#275
deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
LiHua000:master

Conversation

@LiHua000
Copy link
Copy Markdown
Contributor

Network shares (SMB/NFS) cause extreme slowness due to libdjvu's
random seek I/O pattern. Detect network paths via QStorageInfo and
copy to local temp dir first, similar to how DOCX is handled.

log: fix bug

Bug: https://pms.uniontech.com/bug-view-354845.html

Network shares (SMB/NFS) cause extreme slowness due to libdjvu's
  random seek I/O pattern. Detect network paths via QStorageInfo and
  copy to local temp dir first, similar to how DOCX is handled.

log: fix bug

Bug: https://pms.uniontech.com/bug-view-354845.html
@deepin-ci-robot
Copy link
Copy Markdown

deepin pr auto review

你好!我是CodeGeeX。我已仔细审查了你提交的Git Diff。本次代码变更的主要目的是解决DjVu文件在网络路径下无法正常打开或性能低下的问题,通过将其复制到本地临时目录来规避底层库对网络文件系统支持不佳的缺陷

整体思路非常清晰,但在语法逻辑、代码质量、性能和安全性方面,我发现了几个需要改进的关键点。以下是详细的审查意见:

1. 语法与逻辑

  • 临时文件命名冲突:在 Model.cpp 中,将网络文件硬编码复制为 temp.djvu。如果用户同时打开两个不同的网络DjVu文件,或者由于上次异常退出导致 temp.djvu 残留,直接 QFile::remove 并覆盖会导致数据混乱或覆盖其他正在打开的文件
    • 建议:使用 QTemporaryFile 或基于原文件名生成带有唯一标识(如MD5哈希或UUID)的临时文件名,例如 temp_XXXX.djvu
  • isNetworkPath 判断逻辑存在误判风险:在 Global.cpp 中,storage.isRoot() 的判断逻辑存在风险。如果用户将网络驱动器挂载到了根目录(虽然罕见),或者 QStorageInfo 对某些挂载点返回了意外的根属性,该函数会错误地返回 false。另外,filePath.startsWith("/run/user/") && filePath.contains("/gvfs/") 这个判断过于依赖具体的文件系统路径结构,可移植性较差。
    • 建议:移除 storage.isRoot() 判断,只要文件系统类型匹配就应返回 true。对于 GVFS,判断 fsType.contains("fuse") 或许是更底层的通用解法,如果必须保留路径判断,建议增加更详尽的注释说明为何要这么做。

2. 代码质量

  • 硬编码的魔法字符串"temp.djvu"Model.cppDocSheet.cpp 中均出现了硬编码。如果未来修改了临时文件命名规则,容易遗漏。
    • 建议:在 DocSheet 或全局定义中将该相对路径提取为常量,确保读取和写入路径一致。
  • QStorageInfo 头文件重复包含QStorageInfo 分别在 Global.cppGlobal.h 中被 include。虽然不会报错,但属于冗余。
    • 建议:由于 isNetworkPath 函数签名仅涉及 QString,在 Global.h 中不需要 QStorageInfo,移除 Global.h 中的 #include <QStorageInfo>,仅在 Global.cpp 中保留即可。

3. 代码性能

  • QStorageInfo 的构造开销QStorageInfo storage(filePath) 在某些操作系统上可能会引发系统调用(如 statvfs),以获取文件系统的详细信息。如果 isNetworkPath 被频繁调用,可能会产生性能瓶颈。
    • 建议:确保该函数仅在加载文档等低频操作中调用。如果在渲染等高频逻辑中被调用,需要考虑增加缓存机制(例如使用 QHash<QString, bool> 缓存路径的网络状态判定结果)。

4. 代码安全

  • 不安全的文件删除与覆盖QFile::remove(localFilePath)QFile::copy 存在竞态条件。在高并发或恶意环境下,攻击者可能在 removecopy 之间创建一个同名的链接文件,导致程序误写数据到其他敏感位置。
    • 建议:使用 QTemporaryFile 自动生成安全的随机临时文件名,避免手动管理文件生命周期和覆盖逻辑。
  • 临时文件残留:目前的代码只负责创建 temp.djvu,但没有看到在文档关闭时清理该临时文件的逻辑(可能在其他未展示的代码中)。如果未清理,长期使用会导致本地存储空间被大量网络文件副本耗尽。
    • 建议:确认在 DocSheet 析构或关闭时,有对应的 QFile::remove 逻辑清理该副本。

改进后的代码建议

基于以上分析,我为你提供了修改后的代码片段:

reader/app/Global.h

移除不必要的头文件包含:

#include "DebugTimeManager.h"

#include <QString>
// 删除: #include <QStorageInfo>

namespace Dr {
// ... 其他代码保持不变

reader/app/Global.cpp

优化网络路径判断逻辑,移除 isRoot() 判断:

#include <QStorageInfo>
// ... 其他头文件

bool isNetworkPath(const QString &filePath)
{
    const QStorageInfo storage(filePath);
    if (!storage.isValid()) // 移除 storage.isRoot() 判断,避免误判挂载在根目录的网络驱动器
        return false;

    const QByteArray fsType = storage.fileSystemType().toLower();
    // 增加对 fuse.sshfs 的匹配,因为有些系统带fuse前缀
    if (fsType.contains("nfs") || fsType.contains("cifs") ||
        fsType.contains("smb") || fsType.contains("sshfs") || fsType.contains("fuse.sshfs") ||
        fsType.contains("gvfsd") || fsType.contains("fuse.gvfsd-fuse"))
        return true;

    // 保留对 GVFS 特定路径的兼容,但建议未来观察是否可通过 fsType 完全替代
    if (filePath.startsWith("/run/user/") && filePath.contains("/gvfs/"))
        return true;

    return false;
}

reader/document/Model.cpp

使用安全的临时文件创建方式,避免命名冲突和覆盖问题:

#include "Global.h"
#include <QTemporaryFile> // 新增

// ... 在 getDocument 函数中
    } else if (Dr::DJVU == fileType) {
        qCDebug(appLog) << "Handling DJVU document";
        if (Dr::isNetworkPath(filePath) && !convertedFileDir.isEmpty()) {
            qCInfo(appLog) << "DJVU file is on network path, copying to local temp dir";
            
            // 使用 QTemporaryFile 生成安全的随机临时文件名,避免冲突和竞态条件
            QTemporaryFile tempFile(convertedFileDir + "/XXXXXX.djvu");
            if (!tempFile.open()) {
                qCritical() << "Failed to create temp file for network DJVU";
                error = deepin_reader::Document::FileError;
            } else {
                QString localFilePath = tempFile.fileName();
                // QTemporaryFile 在析构时会删除文件,但我们需要 DjVuDocument 持续使用它
                // 因此必须取消自动删除,交由 DocSheet 生命周期管理清理
                tempFile.setAutoRemove(false); 
                tempFile.close(); // 关闭句柄以便 QFile::copy 写入

                if (!QFile::copy(filePath, localFilePath)) {
                    qCritical() << "Failed to copy network DJVU file from" << filePath << "to" << localFilePath;
                    QFile::remove(localFilePath); // 失败时清理空文件
                    error = deepin_reader::Document::FileError;
                } else {
                    qCInfo(appLog) << "Copied network DJVU to local:" << localFilePath;
                    document = deepin_reader::DjVuDocument::loadDocument(localFilePath, error, filePath);
                }
            }
        } else {
            document = deepin_reader::DjVuDocument::loadDocument(filePath, error);
        }

reader/uiframe/DocSheet.cpp

注意:由于我们在 Model 中使用了动态随机文件名,DocSheet 必须能够动态获取这个真实的本地路径,而不是硬编码拼接 temp.djvu
建议在 DjVuDocumentDocument 基类中增加一个 localFilePath() 的方法,或者将动态生成的路径缓存起来。

如果架构上难以立刻修改,作为退让方案,至少要确保文件名包含原文件名的哈希值以减少冲突,并在 DocSheet 中保持一致。假设你仍想使用固定目录下的特定名称,请至少加上原文件名的区分度:

退让方案(不推荐,仅作过渡):

// Model.cpp 中
QString localFilePath = convertedFileDir + "/" + QCryptographicHash::hash(filePath.toUtf8(), QCryptographicHash::Md5).toHex() + ".djvu";
// 注意:此时仍需处理并发覆盖问题

// DocSheet.cpp 中
if (Dr::DJVU == fileType() && Dr::isNetworkPath(filePath()))
    return convertedFileDir() + "/" + QCryptographicHash::hash(filePath().toUtf8(), QCryptographicHash::Md5).toHex() + ".djvu";

最佳方案:让 loadDocument 返回的对象自身记录它实际加载的本地绝对路径,DocSheet::openedFilePath() 直接向 document 对象查询该路径。

@github-actions
Copy link
Copy Markdown

  • 敏感词检查失败, 检测到1个文件存在敏感词
详情
{
    "reader/app/Global.h": [
        {
            "line": "const QString key_up               = \"Up\";",
            "line_number": 108,
            "rule": "S106",
            "reason": "Var naming | 20293a700f"
        },
        {
            "line": "const QString key_down             = \"Down\";",
            "line_number": 109,
            "rule": "S106",
            "reason": "Var naming | 21f2799b08"
        },
        {
            "line": "const QString key_left             = \"Left\";",
            "line_number": 110,
            "rule": "S106",
            "reason": "Var naming | ebc87ca254"
        },
        {
            "line": "const QString key_right            = \"Right\";",
            "line_number": 111,
            "rule": "S106",
            "reason": "Var naming | ce51450b6e"
        },
        {
            "line": "const QString key_space            = \"Space\";           // 空格用于停止启动幻灯片播放",
            "line_number": 112,
            "rule": "S106",
            "reason": "Var naming | 19eddccbbe"
        },
        {
            "line": "const QString key_pgUp             = \"PgUp\";            // 上一页",
            "line_number": 113,
            "rule": "S106",
            "reason": "Var naming | 9c0713cb36"
        },
        {
            "line": "const QString key_pgDown           = \"PgDown\";          // 下一页",
            "line_number": 114,
            "rule": "S106",
            "reason": "Var naming | 73cb3b091d"
        },
        {
            "line": "const QString key_delete           = \"Del\";             // 删除",
            "line_number": 115,
            "rule": "S106",
            "reason": "Var naming | 8309604c6a"
        },
        {
            "line": "const QString key_esc              = \"Esc\";             // 退出全屏\退出放映\退出放大镜",
            "line_number": 116,
            "rule": "S106",
            "reason": "Var naming | 7dc55b084c"
        },
        {
            "line": "const QString key_f1               = \"F1\";              // 帮助",
            "line_number": 117,
            "rule": "S106",
            "reason": "Var naming | cfc950dca5"
        },
        {
            "line": "const QString key_f5               = \"F5\";              // 播放幻灯片",
            "line_number": 118,
            "rule": "S106",
            "reason": "Var naming | 8ce983fd5e"
        },
        {
            "line": "const QString key_f11              = \"F11\";             // 全屏",
            "line_number": 119,
            "rule": "S106",
            "reason": "Var naming | a2272d0c5c"
        },
        {
            "line": "const QString key_ctrl_1           = \"Ctrl+1\";          // 适合页面状态",
            "line_number": 120,
            "rule": "S106",
            "reason": "Var naming | 298036410c"
        },
        {
            "line": "const QString key_ctrl_2           = \"Ctrl+2\";          // 适合高度",
            "line_number": 121,
            "rule": "S106",
            "reason": "Var naming | bb89826f32"
        },
        {
            "line": "const QString key_ctrl_3           = \"Ctrl+3\";          // 适合宽度",
            "line_number": 122,
            "rule": "S106",
            "reason": "Var naming | 0ec78a6f33"
        },
        {
            "line": "const QString key_ctrl_d           = \"Ctrl+D\";          // 添加书签",
            "line_number": 123,
            "rule": "S106",
            "reason": "Var naming | 55b590dac6"
        },
        {
            "line": "const QString key_ctrl_f           = \"Ctrl+F\";          // 搜索",
            "line_number": 124,
            "rule": "S106",
            "reason": "Var naming | c37c14ca83"
        },
        {
            "line": "const QString key_ctrl_o           = \"Ctrl+O\";          // 打开",
            "line_number": 125,
            "rule": "S106",
            "reason": "Var naming | 71c792ad13"
        },
        {
            "line": "const QString key_ctrl_e           = \"Ctrl+E\";          // 导出",
            "line_number": 126,
            "rule": "S106",
            "reason": "Var naming | f21c130ba1"
        },
        {
            "line": "const QString key_ctrl_p           = \"Ctrl+P\";          // 打印",
            "line_number": 127,
            "rule": "S106",
            "reason": "Var naming | c6591979a9"
        },
        {
            "line": "const QString key_ctrl_s           = \"Ctrl+S\";          // 保存",
            "line_number": 128,
            "rule": "S106",
            "reason": "Var naming | 7c224c84ee"
        },
        {
            "line": "const QString key_ctrl_m           = \"Ctrl+M\";          // 打开目标缩略图",
            "line_number": 129,
            "rule": "S106",
            "reason": "Var naming | f723ab894f"
        },
        {
            "line": "const QString key_ctrl_r           = \"Ctrl+R\";          // 左旋转",
            "line_number": 130,
            "rule": "S106",
            "reason": "Var naming | d3f2cb7d2b"
        },
        {
            "line": "const QString key_ctrl_c           = \"Ctrl+C\";",
            "line_number": 131,
            "rule": "S106",
            "reason": "Var naming | 7191938108"
        },
        {
            "line": "const QString key_ctrl_x           = \"Ctrl+X\";",
            "line_number": 132,
            "rule": "S106",
            "reason": "Var naming | a6dd4ba68a"
        },
        {
            "line": "const QString key_ctrl_v           = \"Ctrl+V\";",
            "line_number": 133,
            "rule": "S106",
            "reason": "Var naming | 241a1788e0"
        },
        {
            "line": "const QString key_ctrl_z           = \"Ctrl+Z\";",
            "line_number": 134,
            "rule": "S106",
            "reason": "Var naming | f78601e55e"
        },
        {
            "line": "const QString key_ctrl_a           = \"Ctrl+A\";",
            "line_number": 135,
            "rule": "S106",
            "reason": "Var naming | 4774d3cc37"
        },
        {
            "line": "const QString key_ctrl_equal       = \"Ctrl+=\";          // 放大",
            "line_number": 136,
            "rule": "S106",
            "reason": "Var naming | 51a1643479"
        },
        {
            "line": "const QString key_ctrl_smaller     = \"Ctrl+-\";          // 缩小",
            "line_number": 137,
            "rule": "S106",
            "reason": "Var naming | 2c983162a1"
        },
        {
            "line": "const QString key_alt_1            = \"Alt+1\";           // 选择工具",
            "line_number": 138,
            "rule": "S106",
            "reason": "Var naming | a0378f280b"
        },
        {
            "line": "const QString key_alt_2            = \"Alt+2\";           // 手型工具",
            "line_number": 139,
            "rule": "S106",
            "reason": "Var naming | 57ced1cd00"
        },
        {
            "line": "const QString key_alt_a            = \"Alt+A\";           // 添加注释",
            "line_number": 140,
            "rule": "S106",
            "reason": "Var naming | 3c60d1d86d"
        },
        {
            "line": "const QString key_alt_h            = \"Alt+H\";           // 添加高亮",
            "line_number": 141,
            "rule": "S106",
            "reason": "Var naming | 1878434593"
        },
        {
            "line": "const QString key_alt_z            = \"Alt+Z\";           // 放大镜",
            "line_number": 142,
            "rule": "S106",
            "reason": "Var naming | e48b2efc36"
        },
        {
            "line": "const QString key_alt_f4           = \"Alt+F4\";          // 退出应用程序",
            "line_number": 143,
            "rule": "S106",
            "reason": "Var naming | d225da4d6e"
        },
        {
            "line": "const QString key_alt_harger       = \"Ctrl++\";          // 放大",
            "line_number": 144,
            "rule": "S106",
            "reason": "Var naming | 9e19ceeedd"
        },
        {
            "line": "const QString key_ctrl_shift_r     = \"Ctrl+Shift+R\";    // 右旋转",
            "line_number": 145,
            "rule": "S106",
            "reason": "Var naming | 6be0727d79"
        },
        {
            "line": "const QString key_ctrl_shift_s     = \"Ctrl+Shift+S\";    // 另存为",
            "line_number": 146,
            "rule": "S106",
            "reason": "Var naming | 8f81237f85"
        },
        {
            "line": "const QString key_ctrl_shift_slash = \"Ctrl+Shift+/\";",
            "line_number": 147,
            "rule": "S106",
            "reason": "Var naming | bad1d49dcc"
        },
        {
            "line": "const QString key_ctrl_home        = \"Ctrl+Home\";       //第一页",
            "line_number": 148,
            "rule": "S106",
            "reason": "Var naming | bff733899f"
        },
        {
            "line": "const QString key_ctrl_end         = \"Ctrl+End\";        //最后一页",
            "line_number": 149,
            "rule": "S106",
            "reason": "Var naming | 7cbe7a3f8b"
        }
    ]
}

@deepin-ci-robot
Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: LiHua000, lzwind

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@LiHua000
Copy link
Copy Markdown
Contributor Author

/merge

@deepin-bot deepin-bot Bot merged commit 40b94c4 into linuxdeepin:master May 14, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants