|
|
|
|
@ -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); |
|
|
|
|
} |
|
|
|
|
|