Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 52 additions & 3 deletions src/plugins/data-transfer/core/utils/settinghepler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Comment on lines +308 to +313

Copy link
Copy Markdown

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.

Suggested change
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)) {
for (const auto &entry : entries) {
const QString target = dst + QLatin1Char('/') + entry.fileName();
if (entry.isSymLink())
return false;
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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. moveFile then returns false, but a retry sees that partial destination as an existing collision and redirects the remaining source to a new (1) path instead of replacing or resuming the incomplete tree.

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
}
WLOG << "moveFile error: rename dir failed: " << src.toStdString() << " -> " << dst.toStdString();
return false;
}
QDir(dst).removeRecursively();
WLOG << "moveFile error: rename dir failed: " << src.toStdString() << " -> " << dst.toStdString();
return false;

}
// ---------- 文件处理 ----------
// 优先 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 QDir(src).removeRecursively() or QFile::remove(src) fails, both source and destination remain while callers report “Transfer completed”, so a later retry can duplicate the data or leave the migration incomplete.

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;
Expand Down
Loading