-
Notifications
You must be signed in to change notification settings - Fork 36
fix(data-transfer): fallback to copy when moveFile crosses filesystems #786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -294,30 +294,79 @@ bool SettingHelper::setFile(QJsonObject jsonObj, QString filepath) | |||||||||||||||||||
| return true; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // 递归复制目录, 用于跨设备(EXDEV) rename 失败时的退化搬运 | ||||||||||||||||||||
| static bool copyDirectory(const QString &src, const QString &dst) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| QDir srcDir(src); | ||||||||||||||||||||
| if (!srcDir.exists()) | ||||||||||||||||||||
| return false; | ||||||||||||||||||||
| if (!QDir().mkpath(dst)) | ||||||||||||||||||||
| return false; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| const auto entries = srcDir.entryInfoList( | ||||||||||||||||||||
| QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System); | ||||||||||||||||||||
| for (const auto &entry : entries) { | ||||||||||||||||||||
| const QString target = dst + QLatin1Char('/') + entry.fileName(); | ||||||||||||||||||||
| if (entry.isDir()) { | ||||||||||||||||||||
| if (!copyDirectory(entry.absoluteFilePath(), target)) | ||||||||||||||||||||
| return false; | ||||||||||||||||||||
| } else if (!QFile::copy(entry.absoluteFilePath(), target)) { | ||||||||||||||||||||
| WLOG << "copy file failed: " << entry.absoluteFilePath().toStdString() | ||||||||||||||||||||
| << " -> " << target.toStdString(); | ||||||||||||||||||||
| return false; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return true; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| bool SettingHelper::moveFile(const QString &src, QString &dst) | ||||||||||||||||||||
| { | ||||||||||||||||||||
| if (QFile::exists(dst)) { | ||||||||||||||||||||
| int i = 1; | ||||||||||||||||||||
| QString fileName = dst.split("/").last(); | ||||||||||||||||||||
| QString dstDir = dst.remove(fileName); | ||||||||||||||||||||
| QString fileName = dst.section('/', -1); // 只取结尾的文件名 | ||||||||||||||||||||
| QString dstDir = dst.left(dst.size() - fileName.size()); // 只截掉结尾段,避免误删路径中间的同名段 | ||||||||||||||||||||
| QStringList filenamelist = fileName.split("."); | ||||||||||||||||||||
| QString suffix; | ||||||||||||||||||||
| if (!QFileInfo(src).isDir() && filenamelist.size() >= 2) { | ||||||||||||||||||||
| suffix = filenamelist.last(); | ||||||||||||||||||||
| suffix = "." + suffix; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| QString baseName = fileName; | ||||||||||||||||||||
| baseName = fileName.remove(suffix); | ||||||||||||||||||||
| if (baseName.endsWith(suffix)) // 只去掉结尾的后缀 | ||||||||||||||||||||
| baseName.chop(suffix.size()); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| while (QFile::exists(dst)) { | ||||||||||||||||||||
| dst = dstDir + "/" + baseName + "(" + QString::number(i) + ")" + suffix; | ||||||||||||||||||||
| i++; | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
| // 兜底确保目标父目录存在(如 ~/D: 这类盘符目录) | ||||||||||||||||||||
| QDir().mkpath(QFileInfo(dst).absolutePath()); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| // ---------- 目录处理 ---------- | ||||||||||||||||||||
| // 优先 QDir::rename 移动; 跨设备(EXDEV) rename 失败时, 退化为递归复制整棵目录树后删除源 | ||||||||||||||||||||
| if (QFileInfo(src).isDir()) { | ||||||||||||||||||||
| QDir dir; | ||||||||||||||||||||
| if (dir.rename(src, dst)) | ||||||||||||||||||||
| return true; | ||||||||||||||||||||
| if (copyDirectory(src, dst)) { | ||||||||||||||||||||
| QDir(src).removeRecursively(); // 数据已就位, 源清理失败不影响结果 | ||||||||||||||||||||
| return true; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| WLOG << "moveFile error: rename dir failed: " << src.toStdString() << " -> " << dst.toStdString(); | ||||||||||||||||||||
| return false; | ||||||||||||||||||||
|
Comment on lines
+355
to
+358
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): A failed recursive copy leaves the already-created destination tree in place. Triggers: When copying any child entry fails after earlier entries have already been copied. Suggested fix: Remove the partial destination tree before returning failure, or use a temporary destination and atomically rename it into place only after the full copy succeeds.
Suggested change
|
||||||||||||||||||||
| } | ||||||||||||||||||||
| // ---------- 文件处理 ---------- | ||||||||||||||||||||
| // 优先 rename 移动; 跨设备时退化为复制后删除源 | ||||||||||||||||||||
| QFile f(src); | ||||||||||||||||||||
| LOG << "moveFile dst: " << src.toStdString() << " " << dst.toStdString(); | ||||||||||||||||||||
| if (f.rename(dst)) | ||||||||||||||||||||
| return true; | ||||||||||||||||||||
| if (QFile::copy(src, dst)) { // 跨设备时退化为复制后删除源 | ||||||||||||||||||||
| QFile::remove(src); | ||||||||||||||||||||
|
Comment on lines
+353
to
+367
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): The copy-then-delete fallback returns true without checking whether source cleanup succeeded. If Triggers: When the source cannot be removed after a successful copy, such as due to permissions, read-only storage, or a file being in use. Suggested fix: Return false when source removal fails, or explicitly surface the cleanup failure and prevent the operation from being reported as complete. |
||||||||||||||||||||
| return true; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| WLOG << "moveFile error: " << f.errorString().toStdString(); | ||||||||||||||||||||
| return false; | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): The recursive fallback follows directory symlinks because it classifies entries with
entry.isDir()and does not exclude symbolic links. A symlink to an external directory is copied recursively outside the source tree, while a self-referential or cyclic symlink causes unbounded recursion until the copy fails, leaving an incomplete destination.Triggers: When the transferred directory contains a symbolic link to a directory.
Suggested fix: Detect and handle symlinks explicitly, preserving the link or rejecting it, and exclude them from recursive directory traversal.