Browse Source

Add sequential member file uploads with destination locking

master
evgeny 4 days ago
parent
commit
b631c4ed72
  1. 163
      tools/chatgui/src/member_files/browser.cpp
  2. 13
      tools/chatgui/src/member_files/browser.h

163
tools/chatgui/src/member_files/browser.cpp

@ -21,6 +21,7 @@
#include <QUrl>
#include <QMenu>
#include <QTimer>
#include <QPointer>
#include <QLocale>
#include <limits>
#include <cstring>
@ -54,10 +55,11 @@ MemberFileBrowser::MemberFileBrowser(DbManager* db, QWidget* parent) : QDialog(p
m_quota->setObjectName("memberFilesQuota"); m_quota->setWordWrap(true); layout->addWidget(m_quota);
auto* quotaTimer = new QTimer(this); quotaTimer->setInterval(2000);
connect(quotaTimer, &QTimer::timeout, this, [this]() { if (isVisible()) requestQuota(); }); quotaTimer->start();
auto* navigation = new QHBoxLayout;
m_navigation = new QWidget(this);
auto* navigation = new QHBoxLayout(m_navigation); navigation->setContentsMargins(0, 0, 0, 0);
auto* up = new QPushButton(text("Up"), this); auto* reload = new QPushButton(text("Refresh"), this);
m_location = new QLineEdit(this); m_location->setPlaceholderText(text("Path inside the member directory"));
navigation->addWidget(up); navigation->addWidget(m_location, 1); navigation->addWidget(reload); layout->addLayout(navigation);
navigation->addWidget(up); navigation->addWidget(m_location, 1); navigation->addWidget(reload); layout->addWidget(m_navigation);
m_entries = new QTreeWidget(this); m_entries->setRootIsDecorated(false);
m_entries->setHeaderLabels({text("Name"), text("Type"), text("Size"), text("Modified")});
m_entries->header()->setSectionResizeMode(0, QHeaderView::Stretch);
@ -68,6 +70,8 @@ MemberFileBrowser::MemberFileBrowser(DbManager* db, QWidget* parent) : QDialog(p
m_remove = new QPushButton(text("Delete"), this); m_cancel = new QPushButton(text("Cancel transfer"), this);
for (auto* button : {m_upload, m_download, m_mkdir, m_rename, m_remove, m_cancel}) actions->addWidget(button);
layout->addLayout(actions); m_status = new QLabel(this); layout->addWidget(m_status);
m_uploadStatus = new QLabel(this); m_uploadStatus->setObjectName("memberFilesUploadStatus");
m_uploadStatus->setWordWrap(true); layout->addWidget(m_uploadStatus);
m_progress = new QProgressBar(this); m_progress->setRange(0, 1000); m_progress->hide(); layout->addWidget(m_progress);
connect(reload, &QPushButton::clicked, this, [this]() { m_staleRetries = 0; refresh(); });
connect(up, &QPushButton::clicked, this, [this]() {
@ -107,6 +111,10 @@ MemberFileBrowser::~MemberFileBrowser() {
}
void MemberFileBrowser::setChannel(const QString& channel, quint64 selectedOwner) {
if (m_uploadBatchId || m_transferId) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: context change blocked during transfer channel=%s", qPrintable(channel));
m_status->setText(text("Finish or cancel the current transfer first")); return;
}
bool valid = false; quint64 group = channel.toULongLong(&valid);
if (!valid || !group || !m_db) return;
cancelRequest(m_listId); m_listId = 0; m_group = group; m_path.clear();
@ -146,9 +154,11 @@ void MemberFileBrowser::updateActions() {
auto* item = m_entries->currentItem(); bool own = owner() && owner() == m_db->myNodeId();
bool available = server() && gui_bridge_is_uasync_ready();
bool file = item && item->data(0, Qt::UserRole + 1).toInt() == MF_FILE;
m_upload->setEnabled(available && own && !m_transferId); m_mkdir->setEnabled(available && own);
m_rename->setEnabled(available && own && item); m_remove->setEnabled(available && own && item);
m_download->setEnabled(available && owner() && file && !m_transferId); m_cancel->setEnabled(m_transferId != 0);
bool busy = m_uploadBatchId || m_transferId;
m_servers->setEnabled(!busy); m_owners->setEnabled(!busy); m_navigation->setEnabled(!busy); m_entries->setEnabled(!busy);
m_upload->setEnabled(available && own && !busy); m_mkdir->setEnabled(available && own && !busy);
m_rename->setEnabled(available && own && item && !busy); m_remove->setEnabled(available && own && item && !busy);
m_download->setEnabled(available && owner() && file && !busy); m_cancel->setEnabled(busy);
}
bool MemberFileBrowser::post(mf_message& message) {
@ -230,6 +240,38 @@ void MemberFileBrowser::onResponse(const uint8_t* data, int length) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "member_files: GUI response id=%llu list_id=%llu cmd=%s status=%s bytes=%u owner=%llx/%llx path=%s",
(unsigned long long)r.request_id, (unsigned long long)m_listId, mf_command_text(r.command), mf_status_text(r.status), r.data_len,
(unsigned long long)r.owner, (unsigned long long)owner(), r.path);
if (m_uploadBatchId && r.request_id == m_uploadStatId) {
m_uploadStatId = 0;
QString local = m_uploadQueue.first(), name = QFileInfo(local).fileName();
QString remote = m_path.isEmpty() ? name : m_path + '/' + name;
if (r.command != MF_STAT || r.owner != owner() || QString::fromUtf8(r.path) != remote) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file browser: upload STAT context mismatch id=%llu", (unsigned long long)r.request_id);
finishUploads(text("Invalid directory response")); return;
}
if (r.status != MF_OK && r.status != MF_NOT_FOUND) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: upload STAT failed path=%s status=%s", qPrintable(remote), mf_status_text(r.status));
finishUploads(text("Upload stopped: %1 — %2").arg(name, QString::fromUtf8(mf_status_text(r.status)))); return;
}
QByteArray version(16, '\0');
if (r.status == MF_OK) {
if (r.type != MF_FILE) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: upload target is a directory path=%s", qPrintable(remote));
finishUploads(text("Upload stopped: %1 — %2").arg(name, text("A folder with this name already exists"))); return;
}
version = QByteArray((const char*)r.version, 16);
quint64 batch = m_uploadBatchId; QPointer<MemberFileBrowser> guard(this);
auto answer = QMessageBox::question(this, text("Replace file"), text("Replace %1?").arg(name),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No);
if (!guard || m_uploadBatchId != batch) return;
if (answer == QMessageBox::Cancel) { cancel(); return; }
if (answer != QMessageBox::Yes) {
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file browser: upload skipped path=%s", qPrintable(remote));
m_uploadSkipped++; m_uploadQueue.removeFirst(); nextUpload(); return;
}
}
if (!startTransfer(remote, local, true, version)) finishUploads(text("Invalid path or chat is stopped"));
return;
}
if (r.command == MF_QUOTA) {
if (r.request_id != m_quotaId || r.owner != owner()) {
DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "member_files: GUI stale quota response id=%llu", (unsigned long long)r.request_id);
@ -317,17 +359,82 @@ void MemberFileBrowser::onResponse(const uint8_t* data, int length) {
}
void MemberFileBrowser::upload() {
if (m_transferId || !owner() || owner() != m_db->myNodeId()) return;
QString local = QFileDialog::getOpenFileName(this, text("Upload file")); if (local.isEmpty()) return;
QString name = QFileInfo(local).fileName(); QByteArray version(16, '\0');
for (int i = 0; i < m_entries->topLevelItemCount(); i++) {
auto* item = m_entries->topLevelItem(i);
if (item->data(0, Qt::UserRole).toString() != name) continue;
if (item->data(0, Qt::UserRole + 1).toInt() != MF_FILE) { m_status->setText(text("A folder with this name already exists")); return; }
if (QMessageBox::question(this, text("Replace file"), text("Replace %1?").arg(name)) != QMessageBox::Yes) return;
version = item->data(0, Qt::UserRole + 2).toByteArray(); break;
QPointer<MemberFileBrowser> guard(this);
QStringList paths = QFileDialog::getOpenFileNames(this, text("Upload file"));
if (guard && !paths.isEmpty()) beginUploads(paths, false);
}
void MemberFileBrowser::uploadOwnFiles(const QString& channel, const QStringList& paths) {
if (m_uploadBatchId || m_transferId) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: drop rejected while busy files=%d", int(paths.size()));
QMessageBox::warning(this, text("Group files"), text("Finish or cancel the current transfer first")); return;
}
startTransfer(m_path.isEmpty() ? name : m_path + '/' + name, local, true, version);
if (!channel.toULongLong() || !m_db || !m_db->myNodeId()) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: drop has no group or own node channel=%s", qPrintable(channel));
QMessageBox::warning(this, text("Group files"), text("Select a group to upload files")); return;
}
setChannel(channel, m_db->myNodeId());
beginUploads(paths, true);
}
// Сервер выбирается до первого запроса; batch ID защищает вложенные циклы диалогов от отмены/закрытия окна.
void MemberFileBrowser::beginUploads(const QStringList& paths, bool chooseServer) {
if (paths.isEmpty()) return;
if (m_uploadBatchId || m_transferId || !server() || !owner() || owner() != m_db->myNodeId() || !gui_bridge_is_uasync_ready()) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: upload unavailable group=%llu server=%llu owner=%llu busy=%d ready=%d",
(unsigned long long)m_group, (unsigned long long)server(), (unsigned long long)owner(),
bool(m_uploadBatchId || m_transferId), gui_bridge_is_uasync_ready());
QMessageBox::warning(this, text("Group files"), !server() ? text("No media server in this group") : text("Upload unavailable"));
return;
}
for (const auto& path : paths) {
QFileInfo info(path); QString remote = m_path.isEmpty() ? info.fileName() : m_path + '/' + info.fileName();
QByteArray localBytes = path.toUtf8(), remoteBytes = remote.toUtf8();
if (!info.isAbsolute() || info.fileName().isEmpty() || localBytes.contains('\0') || localBytes.size() > MF_PATH_MAX ||
remoteBytes.contains('\0') || !mf_path_valid(remoteBytes.constData(), 0)) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: invalid upload path=%s", qPrintable(path));
QMessageBox::warning(this, text("Group files"), text("Invalid upload path: %1").arg(path)); return;
}
}
m_uploadQueue = paths; m_uploadQueue.removeDuplicates(); m_uploadTotal = m_uploadQueue.size();
m_uploadDone = m_uploadSkipped = 0; m_uploadBatchId = requestId(); updateActions();
if (chooseServer) {
QStringList labels; for (int i = 0; i < m_servers->count(); i++) labels.append(m_servers->itemText(i));
quint64 batch = m_uploadBatchId; QPointer<MemberFileBrowser> guard(this); bool ok = false;
QString selected = QInputDialog::getItem(this, text("Upload files"),
text("Upload %1 file(s) to your root directory in group %2. Select media server:").arg(m_uploadTotal).arg(m_group),
labels, m_servers->currentIndex(), false, &ok);
if (!guard || m_uploadBatchId != batch) return;
if (!ok) { cancel(); return; }
m_servers->setCurrentIndex(labels.indexOf(selected));
}
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file browser: upload queue started batch=%llu group=%llu server=%llu owner=%llu path=%s files=%d",
(unsigned long long)m_uploadBatchId, (unsigned long long)m_group, (unsigned long long)server(),
(unsigned long long)owner(), qPrintable(m_path), m_uploadTotal);
nextUpload();
}
void MemberFileBrowser::nextUpload() {
if (!m_uploadBatchId || m_transferId || m_uploadStatId) return;
if (m_uploadQueue.isEmpty()) {
finishUploads(text("Upload queue finished: %1 uploaded, %2 skipped").arg(m_uploadDone).arg(m_uploadSkipped));
refresh(); return;
}
QString name = QFileInfo(m_uploadQueue.first()).fileName();
m_uploadStatus->setText(text("Uploading %1 of %2: %3").arg(m_uploadDone + m_uploadSkipped + 1).arg(m_uploadTotal).arg(name));
mf_message q{}; q.command = MF_STAT; q.owner = owner();
if (!copyPath(q.path, m_path.isEmpty() ? name : m_path + '/' + name) || !post(q)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file browser: upload STAT could not start name=%s", qPrintable(name));
finishUploads(text("Unable to request directory")); return;
}
m_uploadStatId = q.request_id;
}
void MemberFileBrowser::finishUploads(const QString& status) {
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "file browser: upload queue finished batch=%llu uploaded=%d skipped=%d remaining=%d status=%s",
(unsigned long long)m_uploadBatchId, m_uploadDone, m_uploadSkipped, int(m_uploadQueue.size()), qPrintable(status));
cancelRequest(m_uploadStatId); m_uploadStatId = 0; m_uploadBatchId = 0; m_uploadQueue.clear();
m_uploadStatus->setText(status); updateActions();
}
void MemberFileBrowser::download(bool open) {
@ -338,11 +445,13 @@ void MemberFileBrowser::download(bool open) {
m_openAfter = open; startTransfer(selectedPath(), local, false, selectedVersion());
}
void MemberFileBrowser::startTransfer(const QString& remote, const QString& local, bool uploading, const QByteArray& version) {
bool MemberFileBrowser::startTransfer(const QString& remote, const QString& local, bool uploading, const QByteArray& version) {
auto* q = (mf_transfer_request*)u_calloc(1, sizeof(mf_transfer_request));
if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file browser: transfer allocation failed"); return; }
if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file browser: transfer allocation failed"); return false; }
if (!copyPath(q->remote, remote) || !copyPath(q->local, local) || !mf_path_valid(q->remote, 0) || !gui_bridge_is_uasync_ready()) {
u_free(q); m_status->setText(text("Invalid path or chat is stopped")); return;
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: transfer rejected remote=%s local=%s ready=%d",
qPrintable(remote), qPrintable(local), gui_bridge_is_uasync_ready());
u_free(q); m_status->setText(text("Invalid path or chat is stopped")); return false;
}
q->inst = gui_bridge_get_inst(); q->id = requestId(); q->group = m_group; q->server = server(); q->owner = owner(); q->upload = uploading;
if (version.size() == 16) memcpy(q->version, version.constData(), 16);
@ -350,6 +459,7 @@ void MemberFileBrowser::startTransfer(const QString& remote, const QString& loca
if (uploading) m_openAfter = false;
m_progress->setRange(0, 0); m_progress->show(); m_status->setText(text("Preparing transfer…")); updateActions();
gui_bridge_post_uasync_fn(member_files_gui_transfer, q);
return true;
}
void MemberFileBrowser::onTransfer(const uint8_t* data, int length) {
@ -359,6 +469,18 @@ void MemberFileBrowser::onTransfer(const uint8_t* data, int length) {
m_status->setText(text("Transferred %1 / %2 bytes").arg(done).arg(total));
if (data[50] != 2) return;
m_transferId = 0; m_progress->hide(); updateActions();
if (m_uploadBatchId) {
if (status) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "file browser: upload failed local=%s status=%s", qPrintable(m_local), mf_status_text(status));
QString reason = status == MF_INVALID_PATH ? text("Only regular files with valid paths can be uploaded") :
QString::fromUtf8(mf_status_text(status));
finishUploads(text("Upload stopped: %1 — %2").arg(QFileInfo(m_local).fileName(), reason));
} else {
m_uploadDone++; m_uploadQueue.removeFirst(); nextUpload();
}
if (!m_quotaId) requestQuota();
return;
}
if (status) m_status->setText(QString::fromUtf8(mf_status_text((int)status)));
else {
m_status->setText(text("Transfer completed"));
@ -391,4 +513,7 @@ void MemberFileBrowser::mutate(uint8_t command) {
if (post(q)) m_actions.insert(q.request_id, command);
}
void MemberFileBrowser::cancel() { cancelRequest(m_transferId); }
void MemberFileBrowser::cancel() {
if (m_uploadBatchId) finishUploads(text("Upload queue cancelled"));
cancelRequest(m_transferId);
}

13
tools/chatgui/src/member_files/browser.h

@ -5,6 +5,7 @@
#include <QDialog>
#include <QHash>
#include <QByteArray>
#include <QStringList>
#include <cstdint>
class DbManager;
class QComboBox;
@ -22,6 +23,8 @@ public:
explicit MemberFileBrowser(DbManager* db, QWidget* parent = nullptr);
~MemberFileBrowser() override;
void setChannel(const QString& channel, quint64 owner = 0);
// Drop открывает корень своей ноды и предлагает сервер; активная очередь сохраняет своё назначение.
void uploadOwnFiles(const QString& channel, const QStringList& paths);
void onResponse(const uint8_t* data, int length);
void onTransfer(const uint8_t* data, int length);
private:
@ -36,8 +39,11 @@ private:
QByteArray selectedVersion() const;
bool post(struct mf_message& message);
void upload();
void beginUploads(const QStringList& paths, bool chooseServer);
void nextUpload();
void finishUploads(const QString& status);
void download(bool open);
void startTransfer(const QString& remote, const QString& local, bool upload, const QByteArray& version);
bool startTransfer(const QString& remote, const QString& local, bool upload, const QByteArray& version);
void mutate(uint8_t command);
void cancel();
void cancelRequest(quint64 id);
@ -47,14 +53,19 @@ private:
quint64 m_transferGroup = 0, m_transferServer = 0;
QString m_path, m_local;
bool m_openAfter = false;
quint64 m_uploadBatchId = 0, m_uploadStatId = 0;
QStringList m_uploadQueue;
int m_uploadDone = 0, m_uploadSkipped = 0, m_uploadTotal = 0;
int m_staleRetries = 0;
QHash<quint64, uint8_t> m_actions;
QComboBox* m_servers;
QComboBox* m_owners;
QLineEdit* m_location;
QWidget* m_navigation;
QTreeWidget* m_entries;
QPushButton *m_upload, *m_download, *m_mkdir, *m_rename, *m_remove, *m_cancel;
QLabel* m_status;
QLabel* m_uploadStatus;
QLabel* m_quota;
QProgressBar* m_progress;
};

Loading…
Cancel
Save