Add sequential member file uploads with destination locking
This commit is contained in:
1 parent
91a3a621d0
commit
b631c4ed72
2 files changed
+156
-20
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
Reference in new issue
Block a user