diff --git a/tools/chatgui/src/member_files/batch.cpp b/tools/chatgui/src/member_files/batch.cpp index eba3b045..9a60cfc2 100644 --- a/tools/chatgui/src/member_files/batch.cpp +++ b/tools/chatgui/src/member_files/batch.cpp @@ -87,7 +87,13 @@ void MemberFileBatch::upload(const QStringList& sources, const QString& destinat // destination — каталог назначения либо родитель временного каталога экспорта. void MemberFileBatch::start(const QList& entries, const QString& destination, bool temporary) { m_queue = entries; - if (m_mode == Remove) { next(); return; } + if (m_mode == Remove) { + for (const auto& entry : entries) { + if (m_seen.contains(entry.remote) || m_seen.size() >= 100000) { finish(text("Invalid directory response")); return; } + m_seen.insert(entry.remote); m_scanning |= entry.directory; + } + next(); return; + } m_work = true; m_pool.start(QRunnable::create([this, destination, temporary] { QString root = destination, error; @@ -120,7 +126,8 @@ void MemberFileBatch::request(uint8_t command) { q->inst = gui_bridge_get_inst(); q->group = m_group; q->server = m_server; auto& r = q->message; r.kind = MF_REQUEST; r.command = command; r.owner = m_owner; r.request_id = nextId(); memcpy(r.path, entry.remote.toUtf8().constData(), entry.remote.toUtf8().size()); - if ((m_mode == Download || command == MF_REMOVE_PATH) && entry.version.size() == 16) memcpy(r.version, entry.version.constData(), 16); + if ((m_mode == Download || command == MF_REMOVE_PATH || command == MF_LIST) && entry.version.size() == 16) + memcpy(r.version, entry.version.constData(), 16); r.cursor = command == MF_LIST ? entry.cursor : 0; m_command = command; m_request = r.request_id; DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "file batch: request id=%llu command=%s path=%s cursor=%llu", (unsigned long long)m_request, mf_command_text(command), r.path, (unsigned long long)r.cursor); @@ -130,14 +137,22 @@ void MemberFileBatch::request(uint8_t command) { void MemberFileBatch::next() { if (m_finished || m_work || m_request || m_transfer) return; if (m_cancelled) { finish(); return; } + if (m_queue.isEmpty() && m_scanning) { + m_scanning = false; m_queue.swap(m_remove); + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file batch: removal scan complete entries=%d", int(m_queue.size())); + } if (m_queue.isEmpty() && !m_verify.isEmpty()) { m_queue = m_verify; m_verify.clear(); } if (m_queue.isEmpty()) { finish(); return; } const auto entry = m_queue.first(); if (!pathBytes(entry.remote, true) || (m_mode != Remove && !pathBytes(entry.local, false)) || - (m_mode == Download && (entry.version.size() != 16 || mf_zero((const uint8_t*)entry.version.constData(), 16)))) { + ((m_mode == Download || (m_mode == Remove && !entry.directory)) && + (entry.version.size() != 16 || mf_zero((const uint8_t*)entry.version.constData(), 16)))) { finish(text("Cannot copy this path: %1").arg(entry.remote)); return; } - progress(text("Processed %1, skipped %2, remaining %3: %4").arg(m_done).arg(m_skipped).arg(m_queue.size()).arg(entry.remote), -1); + if (m_scanning) { + progress(text("Preparing deletion: %1 entries found — %2").arg(m_seen.size()).arg(entry.remote), -1); + if (!entry.directory) { advance(); return; } + } else progress(text("Processed %1, skipped %2, remaining %3: %4").arg(m_done).arg(m_skipped).arg(m_queue.size()).arg(entry.remote), -1); if (m_mode != Download || entry.verify) { request(MF_STAT); return; } if (entry.directory && entry.cursor) { request(MF_LIST); return; } m_work = true; @@ -192,7 +207,9 @@ bool MemberFileBatch::response(const mf_message& r) { (!entry.directory && entry.version != QByteArray((const char*)r.version, 16))) { finish(text("Source changed: %1").arg(entry.remote)); return true; } - entry.version = QByteArray((const char*)r.version, 16); request(MF_REMOVE_PATH); return true; + // Удаление дочерних объектов меняет версию папки; перед REMOVE берём свежую. + entry.version = QByteArray((const char*)r.version, 16); + request(m_scanning ? MF_LIST : MF_REMOVE_PATH); return true; } if (r.command != MF_LIST) { advance(); return true; } if (r.type != MF_DIRECTORY || mf_zero(r.version, 16)) { finish(text("Invalid directory response")); return true; } @@ -217,7 +234,7 @@ bool MemberFileBatch::response(const mf_message& r) { finish(text("Invalid directory response")); return true; } entry.version = QByteArray((const char*)r.version, 16); entry.cursor = r.cursor; - if (!r.more) { auto verify = entry; verify.verify = true; m_verify.append(verify); } + if (!r.more && m_mode == Download) { auto verify = entry; verify.verify = true; m_verify.append(verify); } m_queue.append(children); if (r.more) request(MF_LIST); else advance(); return true; @@ -251,8 +268,15 @@ bool MemberFileBatch::transfer(const uint8_t* data, int length) { } void MemberFileBatch::advance(bool skipped) { - DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file batch: entry completed path=%s skipped=%d", qPrintable(m_queue.first().remote), skipped); - if (!m_queue.first().verify) { if (skipped) m_skipped++; else m_done++; } + if (m_scanning) { + // Обратный порядок полного обхода гарантирует удаление детей раньше родителей. + m_remove.prepend(m_queue.first()); + DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "file batch: removal planned path=%s entries=%d", + qPrintable(m_queue.first().remote), int(m_remove.size())); + } else { + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file batch: entry completed path=%s skipped=%d", qPrintable(m_queue.first().remote), skipped); + if (!m_queue.first().verify) { if (skipped) m_skipped++; else m_done++; } + } m_queue.removeFirst(); QMetaObject::invokeMethod(this, [this] { next(); }, Qt::QueuedConnection); } @@ -268,8 +292,8 @@ void MemberFileBatch::finish(const QString& error) { if (m_finished) return; m_finished = true; bool ok = error.isEmpty() && !m_cancelled; - QString summary = text("Completed: %1, skipped: %2, remaining: %3").arg(m_done).arg(m_skipped).arg(m_queue.size()); - if (!ok) summary = (m_cancelled ? text("Copy cancelled") : error) + " — " + summary; + QString summary = text("Completed: %1, skipped: %2, remaining: %3").arg(m_done).arg(m_skipped).arg(m_queue.size() + m_remove.size()); + if (!ok) summary = (m_cancelled ? (m_mode == Remove ? text("Deletion cancelled") : text("Copy cancelled")) : error) + " — " + summary; if (ok) DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file batch: %s", qPrintable(summary)); else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file batch: %s", qPrintable(summary)); if (!ok && !m_temporary.isEmpty()) { diff --git a/tools/chatgui/src/member_files/batch.h b/tools/chatgui/src/member_files/batch.h index fecb7bdd..9de9dc0f 100644 --- a/tools/chatgui/src/member_files/batch.h +++ b/tools/chatgui/src/member_files/batch.h @@ -44,13 +44,13 @@ private: void cancelRequest(quint64 id); const quint64 m_group, m_server, m_owner; const Mode m_mode; - QList m_queue, m_verify; + QList m_queue, m_verify, m_remove; QSet m_seen; QStringList m_roots; QString m_temporary; QThreadPool m_pool; std::atomic_bool m_cancelled{false}; - bool m_work = false, m_finished = false; + bool m_work = false, m_finished = false, m_scanning = false; quint64 m_request = 0, m_transfer = 0; uint8_t m_command = 0; int m_done = 0, m_skipped = 0, m_policy = 0;