Compare commits
10 Commits
71ea3dbd01
...
3a0529edf8
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a0529edf8 | |||
| d9751a82ff | |||
| 5922ff4ca5 | |||
| 6de6664aa0 | |||
| acade5c35b | |||
| 537f0ddb1e | |||
| b1a81cd90b | |||
| 7c884a2185 | |||
| 2556c2db83 | |||
| bfe271550e |
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
build/
|
||||
*.user
|
||||
*.autosave
|
||||
*.qmlc
|
||||
*.jsc
|
||||
CMakeLists.txt.user
|
||||
cmake-build-*/
|
||||
.idea/
|
||||
.vscode/
|
||||
.cache/
|
||||
@@ -22,6 +22,8 @@ if(WITH_HTTP_SERVER)
|
||||
find_package(Qt6 6.8 COMPONENTS HttpServer)
|
||||
endif()
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
add_subdirectory(src/core)
|
||||
add_subdirectory(src/app)
|
||||
|
||||
@@ -30,3 +32,31 @@ if(BUILD_TESTS)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Test)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
install(DIRECTORY data/icons/
|
||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor
|
||||
)
|
||||
|
||||
install(FILES data/org.localsend.localsend-qt.desktop
|
||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
|
||||
)
|
||||
|
||||
set(CPACK_PACKAGE_NAME "localsend-qt")
|
||||
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Share files between devices on the local network")
|
||||
set(CPACK_PACKAGE_DESCRIPTION "LocalSend Qt is a native Qt6 client for the LocalSend protocol, \
|
||||
allowing fast and secure file sharing between devices on the same local network.")
|
||||
set(CPACK_PACKAGE_HOMEPAGE_URL "https://localsend.org")
|
||||
set(CPACK_PACKAGE_CONTACT "LocalSend Qt Developers")
|
||||
set(CPACK_PACKAGE_LICENSE "MIT")
|
||||
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}_${PROJECT_VERSION}_${CMAKE_SYSTEM_PROCESSOR}")
|
||||
|
||||
set(CPACK_GENERATOR "DEB")
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6core6 (>= 6.8), libqt6network6 (>= 6.8), libqt6quick6 (>= 6.8), libqt6quickcontrols2-6 (>= 6.8), libqt6httpserver6 (>= 6.8), libc6")
|
||||
set(CPACK_DEBIAN_PACKAGE_SECTION "net")
|
||||
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
|
||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
set(CPACK_DEBIAN_PACKAGE_DESCRIPTION "${CPACK_PACKAGE_DESCRIPTION}")
|
||||
set(CPACK_DEBIAN_COMPRESSION_TYPE "zstd")
|
||||
|
||||
include(CPack)
|
||||
|
||||
BIN
data/icons/128x128/apps/localsend-qt.png
Normal file
BIN
data/icons/128x128/apps/localsend-qt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
data/icons/256x256/apps/localsend-qt.png
Normal file
BIN
data/icons/256x256/apps/localsend-qt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
data/icons/512x512/apps/localsend-qt.png
Normal file
BIN
data/icons/512x512/apps/localsend-qt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
12
data/org.localsend.localsend-qt.desktop
Normal file
12
data/org.localsend.localsend-qt.desktop
Normal file
@@ -0,0 +1,12 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=LocalSend Qt
|
||||
GenericName=File Transfer
|
||||
Comment=Share files between devices on the local network
|
||||
Exec=localsend-qt
|
||||
Icon=localsend-qt
|
||||
Terminal=false
|
||||
Categories=Network;FileTransfer;Qt;
|
||||
Keywords=transfer;share;files;local;network;
|
||||
StartupNotify=true
|
||||
StartupWMClass=LocalSend
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QStandardPaths>
|
||||
#include <QUrl>
|
||||
#include <QDebug>
|
||||
#include <QMimeDatabase>
|
||||
|
||||
AppController::AppController(QObject* parent)
|
||||
: QObject(parent)
|
||||
@@ -14,6 +15,7 @@ AppController::AppController(QObject* parent)
|
||||
, m_discovery(new LocalSend::DiscoveryManager(this))
|
||||
, m_server(new LocalSend::HttpServer(this))
|
||||
, m_sessions(new LocalSend::SessionManager(this))
|
||||
, m_httpClient(new LocalSend::HttpClient(this))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -25,6 +27,7 @@ AppController::~AppController()
|
||||
void AppController::initialize()
|
||||
{
|
||||
m_security->initialize();
|
||||
m_server->setSslConfiguration(m_security->sslConfiguration());
|
||||
|
||||
LocalSend::InfoDto info = buildInfoDto();
|
||||
m_server->setLocalInfo(info, m_security->fingerprint());
|
||||
@@ -35,6 +38,8 @@ void AppController::initialize()
|
||||
emit serverRunningChanged();
|
||||
}
|
||||
|
||||
m_server->setReceivePin(m_settings->receivePin());
|
||||
|
||||
connect(m_discovery, &LocalSend::DiscoveryManager::deviceDiscovered,
|
||||
this, &AppController::onDeviceDiscovered);
|
||||
connect(m_discovery, &LocalSend::DiscoveryManager::deviceLost,
|
||||
@@ -45,6 +50,8 @@ void AppController::initialize()
|
||||
this, &AppController::onPrepareUploadRequest);
|
||||
connect(m_server, &LocalSend::HttpServer::uploadRequest,
|
||||
this, &AppController::onUploadRequest);
|
||||
connect(m_server, &LocalSend::HttpServer::cancelRequest,
|
||||
this, &AppController::onCancelRequest);
|
||||
connect(m_sessions, &LocalSend::SessionManager::receiveSessionAccepted,
|
||||
this, &AppController::onSessionAccepted);
|
||||
connect(m_sessions, &LocalSend::SessionManager::receiveSessionDeclined,
|
||||
@@ -54,6 +61,25 @@ void AppController::initialize()
|
||||
connect(m_sessions, &LocalSend::SessionManager::receiveSessionCompleted,
|
||||
this, &AppController::onReceiveCompleted);
|
||||
|
||||
connect(m_httpClient, &LocalSend::HttpClient::prepareUploadResponse,
|
||||
this, &AppController::onPrepareUploadResponse);
|
||||
connect(m_httpClient, &LocalSend::HttpClient::prepareUploadError,
|
||||
this, &AppController::onPrepareUploadError);
|
||||
connect(m_httpClient, &LocalSend::HttpClient::prepareUploadPinRequired,
|
||||
this, &AppController::onPrepareUploadPinRequired);
|
||||
connect(m_httpClient, &LocalSend::HttpClient::prepareUploadTooManyAttempts,
|
||||
this, &AppController::onPrepareUploadTooManyAttempts);
|
||||
connect(m_httpClient, &LocalSend::HttpClient::uploadProgress,
|
||||
this, &AppController::onUploadProgress);
|
||||
connect(m_httpClient, &LocalSend::HttpClient::uploadCompleted,
|
||||
this, &AppController::onUploadCompleted);
|
||||
connect(m_httpClient, &LocalSend::HttpClient::uploadError,
|
||||
this, &AppController::onUploadError);
|
||||
connect(m_httpClient, &LocalSend::HttpClient::registerCompleted,
|
||||
this, &AppController::onManualRegisterCompleted);
|
||||
connect(m_httpClient, &LocalSend::HttpClient::registerError,
|
||||
this, &AppController::onManualRegisterError);
|
||||
|
||||
startDiscovery();
|
||||
}
|
||||
|
||||
@@ -120,6 +146,43 @@ void AppController::setQuickSave(bool enabled)
|
||||
}
|
||||
}
|
||||
|
||||
QString AppController::deviceType() const
|
||||
{
|
||||
return LocalSend::deviceTypeToString(m_settings->deviceType());
|
||||
}
|
||||
|
||||
void AppController::setDeviceType(const QString& type)
|
||||
{
|
||||
LocalSend::DeviceType newType = LocalSend::deviceTypeFromString(type);
|
||||
if (m_settings->deviceType() != newType) {
|
||||
m_settings->setDeviceType(newType);
|
||||
emit deviceTypeChanged();
|
||||
|
||||
LocalSend::InfoDto info = buildInfoDto();
|
||||
m_server->setLocalInfo(info, m_security->fingerprint());
|
||||
m_discovery->setLocalInfo(info, m_security->fingerprint(), m_settings->port(),
|
||||
m_settings->https() ? LocalSend::ProtocolType::Https : LocalSend::ProtocolType::Http);
|
||||
}
|
||||
}
|
||||
|
||||
bool AppController::https() const
|
||||
{
|
||||
return m_settings->https();
|
||||
}
|
||||
|
||||
void AppController::setHttps(bool enabled)
|
||||
{
|
||||
if (m_settings->https() != enabled) {
|
||||
m_settings->setHttps(enabled);
|
||||
emit httpsChanged();
|
||||
|
||||
LocalSend::InfoDto info = buildInfoDto();
|
||||
m_server->setLocalInfo(info, m_security->fingerprint());
|
||||
m_discovery->setLocalInfo(info, m_security->fingerprint(), m_settings->port(),
|
||||
enabled ? LocalSend::ProtocolType::Https : LocalSend::ProtocolType::Http);
|
||||
}
|
||||
}
|
||||
|
||||
QVariantList AppController::devices() const
|
||||
{
|
||||
QVariantList result;
|
||||
@@ -166,6 +229,22 @@ void AppController::acceptReceive(const QString& sessionId)
|
||||
return;
|
||||
}
|
||||
|
||||
m_currentReceiveSessionId = sessionId;
|
||||
m_receiveProgressValue = 0.0;
|
||||
m_receivedSize = 0;
|
||||
m_currentReceiveFileIndex = 0;
|
||||
m_currentReceiveFileName.clear();
|
||||
m_totalReceiveFiles = session.files.size();
|
||||
m_currentReceiveSenderAlias = session.sender.alias;
|
||||
m_totalReceiveSize = 0;
|
||||
m_receiveFileNames.clear();
|
||||
for (auto it = session.files.constBegin(); it != session.files.constEnd(); ++it) {
|
||||
m_totalReceiveSize += it->file.size;
|
||||
m_receiveFileNames.insert(it.key(), it->file.fileName);
|
||||
}
|
||||
emit receivingChanged();
|
||||
emit receiveProgressChanged();
|
||||
|
||||
QString baseDir = downloadPath();
|
||||
QDir dir(baseDir);
|
||||
if (!dir.exists()) {
|
||||
@@ -179,6 +258,7 @@ void AppController::acceptReceive(const QString& sessionId)
|
||||
qDebug() << "[AppController] File" << it.key() << "->" << filePath;
|
||||
}
|
||||
|
||||
m_server->addActiveSession(sessionId);
|
||||
m_sessions->acceptReceiveSession(sessionId, destinationPaths);
|
||||
}
|
||||
|
||||
@@ -255,6 +335,8 @@ void AppController::onPrepareUploadRequest(const QString& httpSessionId,
|
||||
|
||||
LocalSend::Device device;
|
||||
device.ip = sender.toString();
|
||||
device.port = dto.info.port;
|
||||
device.protocol = dto.info.protocol;
|
||||
device.alias = dto.info.alias;
|
||||
device.fingerprint = dto.info.fingerprint;
|
||||
device.deviceModel = dto.info.deviceModel;
|
||||
@@ -318,6 +400,16 @@ void AppController::onUploadRequest(const QString& sessionId, const QString& fil
|
||||
|
||||
m_sessions->updateReceiveProgress(sessionId, fileId, written);
|
||||
|
||||
if (sessionId == m_currentReceiveSessionId) {
|
||||
m_currentReceiveFileName = m_receiveFileNames.value(fileId);
|
||||
m_currentReceiveFileIndex++;
|
||||
m_receivedSize += written;
|
||||
if (m_totalReceiveSize > 0) {
|
||||
m_receiveProgressValue = (static_cast<double>(m_receivedSize) / m_totalReceiveSize) * 100.0;
|
||||
}
|
||||
emit receiveProgressChanged();
|
||||
}
|
||||
|
||||
if (written >= transfer.file.size) {
|
||||
m_sessions->completeReceiveFile(sessionId, fileId);
|
||||
}
|
||||
@@ -343,5 +435,568 @@ void AppController::onReceiveProgress(const QString& sessionId, const QString& f
|
||||
|
||||
void AppController::onReceiveCompleted(const QString& sessionId)
|
||||
{
|
||||
emit receiveCompleted(sessionId);
|
||||
if (sessionId == m_currentReceiveSessionId) {
|
||||
m_receiveProgressValue = 100.0;
|
||||
m_currentReceiveFileIndex = m_totalReceiveFiles;
|
||||
emit receiveProgressChanged();
|
||||
}
|
||||
emit receiveCompleted(sessionId);
|
||||
if (sessionId == m_currentReceiveSessionId) {
|
||||
m_server->removeActiveSession(sessionId);
|
||||
resetReceiveState();
|
||||
}
|
||||
}
|
||||
|
||||
bool AppController::sending() const
|
||||
{
|
||||
return !m_currentSendSessionId.isEmpty();
|
||||
}
|
||||
|
||||
double AppController::sendProgress() const
|
||||
{
|
||||
return m_sendProgress;
|
||||
}
|
||||
|
||||
QVariantList AppController::pendingFiles() const
|
||||
{
|
||||
return m_pendingFilesList;
|
||||
}
|
||||
|
||||
bool AppController::hasPendingFiles() const
|
||||
{
|
||||
return !m_pendingFilesList.isEmpty();
|
||||
}
|
||||
|
||||
QString AppController::receivePin() const
|
||||
{
|
||||
return m_settings->receivePin();
|
||||
}
|
||||
|
||||
void AppController::setReceivePin(const QString& pin)
|
||||
{
|
||||
if (m_settings->receivePin() != pin) {
|
||||
m_settings->setReceivePin(pin);
|
||||
m_server->setReceivePin(pin);
|
||||
emit receivePinChanged();
|
||||
}
|
||||
}
|
||||
|
||||
bool AppController::receiving() const
|
||||
{
|
||||
return !m_currentReceiveSessionId.isEmpty();
|
||||
}
|
||||
|
||||
double AppController::receiveProgress() const
|
||||
{
|
||||
return m_receiveProgressValue;
|
||||
}
|
||||
|
||||
QString AppController::currentReceiveFileName() const
|
||||
{
|
||||
return m_currentReceiveFileName;
|
||||
}
|
||||
|
||||
int AppController::currentReceiveFileIndex() const
|
||||
{
|
||||
return m_currentReceiveFileIndex;
|
||||
}
|
||||
|
||||
int AppController::totalReceiveFiles() const
|
||||
{
|
||||
return m_totalReceiveFiles;
|
||||
}
|
||||
|
||||
QString AppController::currentReceiveSenderAlias() const
|
||||
{
|
||||
return m_currentReceiveSenderAlias;
|
||||
}
|
||||
|
||||
QString AppController::currentSendFileName() const
|
||||
{
|
||||
if (m_currentFileIndex >= 0 && m_currentFileIndex < m_pendingSendPaths.size()) {
|
||||
QFileInfo info(m_pendingSendPaths[m_currentFileIndex]);
|
||||
return info.fileName();
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
int AppController::currentSendFileIndex() const
|
||||
{
|
||||
return m_currentFileIndex + 1;
|
||||
}
|
||||
|
||||
int AppController::totalSendFiles() const
|
||||
{
|
||||
return m_pendingSendPaths.size();
|
||||
}
|
||||
|
||||
void AppController::addFiles(const QStringList& filePaths)
|
||||
{
|
||||
QMimeDatabase mimeDb;
|
||||
for (const QString& rawPath : filePaths) {
|
||||
QString filePath = rawPath;
|
||||
if (filePath.startsWith(QStringLiteral("file://"))) {
|
||||
filePath = filePath.mid(7);
|
||||
}
|
||||
QFileInfo info(filePath);
|
||||
if (!info.exists() || info.isDir()) {
|
||||
continue;
|
||||
}
|
||||
QVariantMap file;
|
||||
file[QStringLiteral("path")] = info.absoluteFilePath();
|
||||
file[QStringLiteral("fileName")] = info.fileName();
|
||||
file[QStringLiteral("size")] = info.size();
|
||||
file[QStringLiteral("fileType")] = mimeDb.mimeTypeForFile(info).name();
|
||||
m_pendingFilesList.append(file);
|
||||
m_pendingSendPaths.append(info.absoluteFilePath());
|
||||
}
|
||||
emit pendingFilesChanged();
|
||||
}
|
||||
|
||||
void AppController::removePendingFile(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_pendingFilesList.size()) {
|
||||
return;
|
||||
}
|
||||
m_pendingFilesList.removeAt(index);
|
||||
m_pendingSendPaths.removeAt(index);
|
||||
emit pendingFilesChanged();
|
||||
}
|
||||
|
||||
void AppController::clearPendingFiles()
|
||||
{
|
||||
m_pendingFilesList.clear();
|
||||
m_pendingSendPaths.clear();
|
||||
emit pendingFilesChanged();
|
||||
}
|
||||
|
||||
void AppController::sendTo(const QString& deviceFingerprint)
|
||||
{
|
||||
if (m_pendingSendPaths.isEmpty()) {
|
||||
emit sendError(QStringLiteral("No files selected"));
|
||||
return;
|
||||
}
|
||||
sendFiles(deviceFingerprint, m_pendingSendPaths);
|
||||
}
|
||||
|
||||
void AppController::sendToAddress(const QString& address)
|
||||
{
|
||||
if (m_pendingSendPaths.isEmpty()) {
|
||||
emit sendError(QStringLiteral("No files selected"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sending()) {
|
||||
emit sendError(QStringLiteral("Already sending files"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString ip = address;
|
||||
quint16 port = m_settings->port();
|
||||
|
||||
if (address.contains(':')) {
|
||||
int colonPos = address.lastIndexOf(':');
|
||||
ip = address.left(colonPos);
|
||||
bool ok = false;
|
||||
int parsedPort = address.mid(colonPos + 1).toInt(&ok);
|
||||
if (ok && parsedPort > 0 && parsedPort <= 65535) {
|
||||
port = static_cast<quint16>(parsedPort);
|
||||
}
|
||||
}
|
||||
|
||||
QHostAddress hostAddr(ip);
|
||||
if (hostAddr.isNull()) {
|
||||
emit sendError(QStringLiteral("Invalid IP address: %1").arg(ip));
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "[AppController] sendToAddress:" << ip << "port:" << port;
|
||||
|
||||
m_manualSendDevice = LocalSend::Device(ip, port);
|
||||
m_manualSendDevice.discoveryMethod = LocalSend::DiscoveryMethod::HttpTarget;
|
||||
m_manualSendPending = true;
|
||||
|
||||
m_httpClient->registerDevice(m_manualSendDevice, buildRegisterDto());
|
||||
}
|
||||
|
||||
void AppController::sendFiles(const QString& deviceFingerprint, const QStringList& filePaths)
|
||||
{
|
||||
if (!m_devices.contains(deviceFingerprint)) {
|
||||
emit sendError(QStringLiteral("Device not found"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (filePaths.isEmpty()) {
|
||||
emit sendError(QStringLiteral("No files selected"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sending()) {
|
||||
emit sendError(QStringLiteral("Already sending files"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_currentSendDeviceFingerprint = deviceFingerprint;
|
||||
m_pendingSendPaths = filePaths;
|
||||
m_currentFileIndex = 0;
|
||||
m_sendProgress = 0.0;
|
||||
m_pinFirstAttempt = true;
|
||||
|
||||
qDebug() << "[AppController] sendFiles: device=" << deviceFingerprint
|
||||
<< "files=" << filePaths.size();
|
||||
|
||||
LocalSend::Device target = m_devices[deviceFingerprint];
|
||||
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
QMimeDatabase mimeDb;
|
||||
|
||||
for (int i = 0; i < filePaths.size(); ++i) {
|
||||
QString filePath = filePaths[i];
|
||||
QFileInfo info(filePath);
|
||||
|
||||
if (!info.exists()) {
|
||||
emit sendError(QStringLiteral("File not found: ") + filePath);
|
||||
resetSendState();
|
||||
return;
|
||||
}
|
||||
|
||||
LocalSend::FileDto fileDto;
|
||||
fileDto.id = QString::number(i);
|
||||
fileDto.fileName = info.fileName();
|
||||
fileDto.size = info.size();
|
||||
fileDto.fileType = mimeDb.mimeTypeForFile(filePath).name();
|
||||
files.insert(fileDto.id, fileDto);
|
||||
}
|
||||
|
||||
m_currentSendSessionId = m_sessions->createSendSession(target, files,
|
||||
[filePaths]() {
|
||||
QMap<QString, QString> paths;
|
||||
for (int i = 0; i < filePaths.size(); ++i) {
|
||||
paths.insert(QString::number(i), filePaths[i]);
|
||||
}
|
||||
return paths;
|
||||
}()
|
||||
);
|
||||
|
||||
emit sendingChanged();
|
||||
emit sendProgressChanged();
|
||||
|
||||
LocalSend::PrepareUploadRequestDto request;
|
||||
request.info = buildRegisterDto();
|
||||
request.files = files;
|
||||
|
||||
qDebug() << "[AppController] Sending prepare-upload request to" << target.ip;
|
||||
m_httpClient->prepareUpload(target, request);
|
||||
}
|
||||
|
||||
void AppController::cancelSend()
|
||||
{
|
||||
if (m_currentSendSessionId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LocalSend::Device target = m_devices.value(m_currentSendDeviceFingerprint);
|
||||
m_httpClient->abortCurrentUpload();
|
||||
m_httpClient->cancel(target, m_currentSendSessionId);
|
||||
|
||||
m_sessions->cancelSendSession(m_currentSendSessionId);
|
||||
resetSendState();
|
||||
m_pendingSendPaths.clear();
|
||||
m_pendingFilesList.clear();
|
||||
emit pendingFilesChanged();
|
||||
}
|
||||
|
||||
void AppController::cancelReceive()
|
||||
{
|
||||
if (m_currentReceiveSessionId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "[AppController] cancelReceive, sessionId:" << m_currentReceiveSessionId;
|
||||
|
||||
LocalSend::ReceiveSession session = m_sessions->receiveSession(m_currentReceiveSessionId);
|
||||
LocalSend::Device senderDevice = session.sender;
|
||||
|
||||
m_sessions->cancelReceiveSession(m_currentReceiveSessionId);
|
||||
m_server->removeActiveSession(m_currentReceiveSessionId);
|
||||
|
||||
if (!senderDevice.ip.isEmpty() && senderDevice.port > 0) {
|
||||
m_httpClient->cancel(senderDevice, m_currentReceiveSessionId);
|
||||
}
|
||||
|
||||
resetReceiveState();
|
||||
}
|
||||
|
||||
void AppController::onPrepareUploadResponse(const LocalSend::PrepareUploadResponseDto& response)
|
||||
{
|
||||
qDebug() << "[AppController] onPrepareUploadResponse: sessionId=" << response.sessionId
|
||||
<< "files=" << response.files.size();
|
||||
|
||||
LocalSend::SendSession session = m_sessions->sendSession(m_currentSendSessionId);
|
||||
if (session.sessionId.isEmpty()) {
|
||||
emit sendError(QStringLiteral("Session not found"));
|
||||
resetSendState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.files.isEmpty()) {
|
||||
emit sendError(QStringLiteral("Receiver declined the transfer"));
|
||||
resetSendState();
|
||||
return;
|
||||
}
|
||||
|
||||
m_sessions->setSendSessionTokens(m_currentSendSessionId, response.sessionId, response.files);
|
||||
|
||||
m_currentSendSessionId = response.sessionId;
|
||||
m_sessions->startSendSession(m_currentSendSessionId);
|
||||
|
||||
m_currentFileIndex = 0;
|
||||
sendNextFile();
|
||||
}
|
||||
|
||||
void AppController::onPrepareUploadError(const QString& error)
|
||||
{
|
||||
qWarning() << "[AppController] onPrepareUploadError:" << error;
|
||||
resetSendState();
|
||||
emit sendError(error);
|
||||
}
|
||||
|
||||
void AppController::onPrepareUploadPinRequired()
|
||||
{
|
||||
qDebug() << "[AppController] onPrepareUploadPinRequired, firstAttempt:" << m_pinFirstAttempt;
|
||||
emit pinRequired(m_pinFirstAttempt);
|
||||
m_pinFirstAttempt = false;
|
||||
}
|
||||
|
||||
void AppController::onPrepareUploadTooManyAttempts()
|
||||
{
|
||||
qWarning() << "[AppController] onPrepareUploadTooManyAttempts";
|
||||
resetSendState();
|
||||
emit sendError(QStringLiteral("Too many PIN attempts"));
|
||||
}
|
||||
|
||||
void AppController::retryWithPin(const QString& pin)
|
||||
{
|
||||
if (m_currentSendDeviceFingerprint.isEmpty() || !m_devices.contains(m_currentSendDeviceFingerprint)) {
|
||||
resetSendState();
|
||||
return;
|
||||
}
|
||||
|
||||
LocalSend::Device target = m_devices[m_currentSendDeviceFingerprint];
|
||||
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
QMimeDatabase mimeDb;
|
||||
|
||||
for (int i = 0; i < m_pendingSendPaths.size(); ++i) {
|
||||
QFileInfo info(m_pendingSendPaths[i]);
|
||||
if (!info.exists()) {
|
||||
continue;
|
||||
}
|
||||
LocalSend::FileDto fileDto;
|
||||
fileDto.id = QString::number(i);
|
||||
fileDto.fileName = info.fileName();
|
||||
fileDto.size = info.size();
|
||||
fileDto.fileType = mimeDb.mimeTypeForFile(info).name();
|
||||
files.insert(fileDto.id, fileDto);
|
||||
}
|
||||
|
||||
LocalSend::PrepareUploadRequestDto request;
|
||||
request.info = buildRegisterDto();
|
||||
request.files = files;
|
||||
|
||||
qDebug() << "[AppController] Retrying prepare-upload with PIN to" << target.ip;
|
||||
m_httpClient->prepareUpload(target, request, pin);
|
||||
}
|
||||
|
||||
void AppController::onUploadProgress(qint64 sent, qint64 total)
|
||||
{
|
||||
if (total > 0) {
|
||||
double fileProgress = static_cast<double>(sent) / total;
|
||||
int totalFiles = m_pendingSendPaths.size();
|
||||
double overallProgress = (m_currentFileIndex + fileProgress) / totalFiles;
|
||||
m_sendProgress = overallProgress * 100.0;
|
||||
emit sendProgressChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void AppController::onUploadCompleted()
|
||||
{
|
||||
qDebug() << "[AppController] onUploadCompleted, file index:" << m_currentFileIndex;
|
||||
|
||||
if (m_currentSendSessionId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_sessions->completeSendFile(m_currentSendSessionId, m_currentSendFileId);
|
||||
|
||||
m_currentFileIndex++;
|
||||
|
||||
if (m_currentFileIndex < m_pendingSendPaths.size()) {
|
||||
sendNextFile();
|
||||
} else {
|
||||
qDebug() << "[AppController] All files sent successfully";
|
||||
m_sendProgress = 100.0;
|
||||
emit sendProgressChanged();
|
||||
QString sessionId = m_currentSendSessionId;
|
||||
resetSendState();
|
||||
m_pendingSendPaths.clear();
|
||||
m_pendingFilesList.clear();
|
||||
emit pendingFilesChanged();
|
||||
emit sendCompleted(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
void AppController::onUploadError(const QString& error)
|
||||
{
|
||||
if (m_currentSendSessionId.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qWarning() << "[AppController] onUploadError:" << error;
|
||||
|
||||
if (error == QStringLiteral("SESSION_CANCELLED")) {
|
||||
m_sessions->cancelSendSession(m_currentSendSessionId);
|
||||
resetSendState();
|
||||
emit sendCanceled();
|
||||
} else {
|
||||
m_sessions->failSendFile(m_currentSendSessionId, m_currentSendFileId);
|
||||
resetSendState();
|
||||
emit sendError(error);
|
||||
}
|
||||
}
|
||||
|
||||
void AppController::sendNextFile()
|
||||
{
|
||||
if (m_currentFileIndex >= m_pendingSendPaths.size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LocalSend::SendSession session = m_sessions->sendSession(m_currentSendSessionId);
|
||||
if (session.sessionId.isEmpty()) {
|
||||
qWarning() << "[AppController] Session not found:" << m_currentSendSessionId;
|
||||
return;
|
||||
}
|
||||
|
||||
QString fileId = QString::number(m_currentFileIndex);
|
||||
if (!session.files.contains(fileId)) {
|
||||
qWarning() << "[AppController] File not found in session:" << fileId;
|
||||
return;
|
||||
}
|
||||
|
||||
m_currentSendFileId = fileId;
|
||||
QString filePath = m_pendingSendPaths[m_currentFileIndex];
|
||||
QString token = session.files[fileId].token;
|
||||
|
||||
LocalSend::Device target = m_devices.value(m_currentSendDeviceFingerprint);
|
||||
|
||||
qDebug() << "[AppController] Uploading file" << fileId << ":" << filePath
|
||||
<< "token:" << token << "to" << target.ip;
|
||||
|
||||
if (token.isEmpty()) {
|
||||
emit sendError(QStringLiteral("No token for file: ") + fileId);
|
||||
resetSendState();
|
||||
return;
|
||||
}
|
||||
|
||||
m_httpClient->uploadFile(target, m_currentSendSessionId, fileId, token, filePath);
|
||||
}
|
||||
|
||||
LocalSend::RegisterDto AppController::buildRegisterDto() const
|
||||
{
|
||||
LocalSend::RegisterDto dto;
|
||||
dto.alias = m_settings->alias();
|
||||
dto.version = m_settings->version();
|
||||
dto.deviceModel = m_settings->deviceModel();
|
||||
dto.deviceType = m_settings->deviceType();
|
||||
dto.fingerprint = m_security->fingerprint();
|
||||
dto.port = m_settings->port();
|
||||
dto.protocol = m_settings->https() ? LocalSend::ProtocolType::Https : LocalSend::ProtocolType::Http;
|
||||
dto.download = false;
|
||||
return dto;
|
||||
}
|
||||
|
||||
void AppController::onCancelRequest(const QString& sessionId)
|
||||
{
|
||||
qDebug() << "[AppController] onCancelRequest, sessionId:" << sessionId;
|
||||
|
||||
if (sessionId == m_currentSendSessionId) {
|
||||
m_httpClient->abortCurrentUpload();
|
||||
m_sessions->cancelSendSession(m_currentSendSessionId);
|
||||
resetSendState();
|
||||
emit sendCanceled();
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionId == m_currentReceiveSessionId) {
|
||||
m_sessions->cancelReceiveSession(m_currentReceiveSessionId);
|
||||
m_server->removeActiveSession(m_currentReceiveSessionId);
|
||||
resetReceiveState();
|
||||
emit receiveError(sessionId, QStringLiteral("Sender canceled the transfer"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void AppController::resetSendState()
|
||||
{
|
||||
m_currentSendSessionId.clear();
|
||||
m_currentSendFileId.clear();
|
||||
m_currentSendDeviceFingerprint.clear();
|
||||
m_currentFileIndex = 0;
|
||||
m_sendProgress = 0.0;
|
||||
emit sendingChanged();
|
||||
emit sendProgressChanged();
|
||||
}
|
||||
|
||||
void AppController::resetReceiveState()
|
||||
{
|
||||
m_currentReceiveSessionId.clear();
|
||||
m_receiveProgressValue = 0.0;
|
||||
m_currentReceiveFileName.clear();
|
||||
m_currentReceiveFileIndex = 0;
|
||||
m_totalReceiveFiles = 0;
|
||||
m_totalReceiveSize = 0;
|
||||
m_receivedSize = 0;
|
||||
m_currentReceiveSenderAlias.clear();
|
||||
m_receiveFileNames.clear();
|
||||
emit receivingChanged();
|
||||
emit receiveProgressChanged();
|
||||
}
|
||||
|
||||
void AppController::onManualRegisterCompleted(const LocalSend::InfoDto& peerInfo)
|
||||
{
|
||||
if (!m_manualSendPending) return;
|
||||
m_manualSendPending = false;
|
||||
|
||||
m_manualSendDevice.alias = peerInfo.alias;
|
||||
m_manualSendDevice.deviceModel = peerInfo.deviceModel;
|
||||
m_manualSendDevice.deviceType = peerInfo.deviceType;
|
||||
m_manualSendDevice.version = peerInfo.version;
|
||||
m_manualSendDevice.fingerprint = peerInfo.fingerprint;
|
||||
|
||||
qDebug() << "[AppController] Manual register completed:" << m_manualSendDevice.alias
|
||||
<< "fingerprint:" << m_manualSendDevice.fingerprint;
|
||||
|
||||
if (!m_devices.contains(m_manualSendDevice.fingerprint)) {
|
||||
m_devices.insert(m_manualSendDevice.fingerprint, m_manualSendDevice);
|
||||
emit devicesChanged();
|
||||
}
|
||||
|
||||
sendFiles(m_manualSendDevice.fingerprint, m_pendingSendPaths);
|
||||
}
|
||||
|
||||
void AppController::onManualRegisterError(const QString& error)
|
||||
{
|
||||
if (!m_manualSendPending) return;
|
||||
|
||||
if (m_manualSendDevice.protocol == LocalSend::ProtocolType::Http) {
|
||||
qWarning() << "[AppController] Manual register failed with HTTP, retrying with HTTPS:" << error;
|
||||
m_manualSendDevice.protocol = LocalSend::ProtocolType::Https;
|
||||
m_httpClient->registerDevice(m_manualSendDevice, buildRegisterDto());
|
||||
return;
|
||||
}
|
||||
|
||||
m_manualSendPending = false;
|
||||
|
||||
qWarning() << "[AppController] Manual register error:" << error;
|
||||
emit sendError(QStringLiteral("Cannot reach device at %1: %2").arg(m_manualSendDevice.ip).arg(error));
|
||||
}
|
||||
|
||||
@@ -19,6 +19,22 @@ class AppController : public QObject
|
||||
Q_PROPERTY(bool serverRunning READ serverRunning NOTIFY serverRunningChanged)
|
||||
Q_PROPERTY(QString downloadPath READ downloadPath WRITE setDownloadPath NOTIFY downloadPathChanged)
|
||||
Q_PROPERTY(bool quickSave READ quickSave WRITE setQuickSave NOTIFY quickSaveChanged)
|
||||
Q_PROPERTY(bool sending READ sending NOTIFY sendingChanged)
|
||||
Q_PROPERTY(double sendProgress READ sendProgress NOTIFY sendProgressChanged)
|
||||
Q_PROPERTY(QString currentSendFileName READ currentSendFileName NOTIFY sendProgressChanged)
|
||||
Q_PROPERTY(int currentSendFileIndex READ currentSendFileIndex NOTIFY sendProgressChanged)
|
||||
Q_PROPERTY(int totalSendFiles READ totalSendFiles NOTIFY sendingChanged)
|
||||
Q_PROPERTY(QVariantList pendingFiles READ pendingFiles NOTIFY pendingFilesChanged)
|
||||
Q_PROPERTY(bool hasPendingFiles READ hasPendingFiles NOTIFY pendingFilesChanged)
|
||||
Q_PROPERTY(QString receivePin READ receivePin WRITE setReceivePin NOTIFY receivePinChanged)
|
||||
Q_PROPERTY(bool receiving READ receiving NOTIFY receivingChanged)
|
||||
Q_PROPERTY(double receiveProgress READ receiveProgress NOTIFY receiveProgressChanged)
|
||||
Q_PROPERTY(QString currentReceiveFileName READ currentReceiveFileName NOTIFY receiveProgressChanged)
|
||||
Q_PROPERTY(int currentReceiveFileIndex READ currentReceiveFileIndex NOTIFY receiveProgressChanged)
|
||||
Q_PROPERTY(int totalReceiveFiles READ totalReceiveFiles NOTIFY receivingChanged)
|
||||
Q_PROPERTY(QString currentReceiveSenderAlias READ currentReceiveSenderAlias NOTIFY receivingChanged)
|
||||
Q_PROPERTY(QString deviceType READ deviceType WRITE setDeviceType NOTIFY deviceTypeChanged)
|
||||
Q_PROPERTY(bool https READ https WRITE setHttps NOTIFY httpsChanged)
|
||||
|
||||
public:
|
||||
explicit AppController(QObject* parent = nullptr);
|
||||
@@ -41,12 +57,45 @@ public:
|
||||
QVariantList devices() const;
|
||||
bool serverRunning() const;
|
||||
|
||||
bool sending() const;
|
||||
double sendProgress() const;
|
||||
QString currentSendFileName() const;
|
||||
int currentSendFileIndex() const;
|
||||
int totalSendFiles() const;
|
||||
QVariantList pendingFiles() const;
|
||||
bool hasPendingFiles() const;
|
||||
QString receivePin() const;
|
||||
void setReceivePin(const QString& pin);
|
||||
|
||||
bool receiving() const;
|
||||
double receiveProgress() const;
|
||||
QString currentReceiveFileName() const;
|
||||
int currentReceiveFileIndex() const;
|
||||
int totalReceiveFiles() const;
|
||||
QString currentReceiveSenderAlias() const;
|
||||
|
||||
QString deviceType() const;
|
||||
void setDeviceType(const QString& type);
|
||||
|
||||
bool https() const;
|
||||
void setHttps(bool enabled);
|
||||
|
||||
Q_INVOKABLE void startDiscovery();
|
||||
Q_INVOKABLE void stopDiscovery();
|
||||
Q_INVOKABLE void refreshDevices();
|
||||
|
||||
Q_INVOKABLE void acceptReceive(const QString& sessionId);
|
||||
Q_INVOKABLE void declineReceive(const QString& sessionId);
|
||||
Q_INVOKABLE void cancelReceive();
|
||||
|
||||
Q_INVOKABLE void sendFiles(const QString& deviceFingerprint, const QStringList& filePaths);
|
||||
Q_INVOKABLE void sendTo(const QString& deviceFingerprint);
|
||||
Q_INVOKABLE void sendToAddress(const QString& address);
|
||||
Q_INVOKABLE void cancelSend();
|
||||
Q_INVOKABLE void addFiles(const QStringList& filePaths);
|
||||
Q_INVOKABLE void removePendingFile(int index);
|
||||
Q_INVOKABLE void clearPendingFiles();
|
||||
Q_INVOKABLE void retryWithPin(const QString& pin);
|
||||
|
||||
signals:
|
||||
void aliasChanged();
|
||||
@@ -55,13 +104,23 @@ signals:
|
||||
void quickSaveChanged();
|
||||
void devicesChanged();
|
||||
void serverRunningChanged();
|
||||
void sendingChanged();
|
||||
void sendProgressChanged();
|
||||
void pendingFilesChanged();
|
||||
void receiveRequest(const QString& sessionId, const QString& senderAlias,
|
||||
const QString& senderIp, const QVariantList& files);
|
||||
void receiveProgress(const QString& sessionId, const QString& fileId, double progress);
|
||||
void receiveCompleted(const QString& sessionId);
|
||||
void receiveError(const QString& sessionId, const QString& error);
|
||||
void sendProgress(const QString& sessionId, double progress);
|
||||
void sendCompleted(const QString& sessionId);
|
||||
void sendError(const QString& error);
|
||||
void sendCanceled();
|
||||
void pinRequired(bool firstAttempt);
|
||||
void receivePinChanged();
|
||||
void receivingChanged();
|
||||
void receiveProgressChanged();
|
||||
void deviceTypeChanged();
|
||||
void httpsChanged();
|
||||
|
||||
private slots:
|
||||
void onDeviceDiscovered(const LocalSend::Device& device);
|
||||
@@ -77,15 +136,54 @@ private slots:
|
||||
void onReceiveProgress(const QString& sessionId, const QString& fileId, double progress);
|
||||
void onReceiveCompleted(const QString& sessionId);
|
||||
|
||||
void onPrepareUploadResponse(const LocalSend::PrepareUploadResponseDto& response);
|
||||
void onPrepareUploadError(const QString& error);
|
||||
void onPrepareUploadPinRequired();
|
||||
void onPrepareUploadTooManyAttempts();
|
||||
void onUploadProgress(qint64 sent, qint64 total);
|
||||
void onUploadCompleted();
|
||||
void onUploadError(const QString& error);
|
||||
void onManualRegisterCompleted(const LocalSend::InfoDto& peerInfo);
|
||||
void onManualRegisterError(const QString& error);
|
||||
|
||||
void onCancelRequest(const QString& sessionId);
|
||||
|
||||
private:
|
||||
LocalSend::Settings* m_settings = nullptr;
|
||||
LocalSend::SecurityContext* m_security = nullptr;
|
||||
LocalSend::DiscoveryManager* m_discovery = nullptr;
|
||||
LocalSend::HttpServer* m_server = nullptr;
|
||||
LocalSend::SessionManager* m_sessions = nullptr;
|
||||
LocalSend::HttpClient* m_httpClient = nullptr;
|
||||
|
||||
QMap<QString, LocalSend::Device> m_devices;
|
||||
|
||||
QString m_currentSendSessionId;
|
||||
QString m_currentSendFileId;
|
||||
QString m_currentSendDeviceFingerprint;
|
||||
QStringList m_pendingSendPaths;
|
||||
QVariantList m_pendingFilesList;
|
||||
int m_currentFileIndex = 0;
|
||||
double m_sendProgress = 0.0;
|
||||
bool m_pinFirstAttempt = true;
|
||||
|
||||
QString m_currentReceiveSessionId;
|
||||
double m_receiveProgressValue = 0.0;
|
||||
QString m_currentReceiveFileName;
|
||||
int m_currentReceiveFileIndex = 0;
|
||||
int m_totalReceiveFiles = 0;
|
||||
qint64 m_totalReceiveSize = 0;
|
||||
qint64 m_receivedSize = 0;
|
||||
QString m_currentReceiveSenderAlias;
|
||||
QMap<QString, QString> m_receiveFileNames;
|
||||
|
||||
LocalSend::InfoDto buildInfoDto() const;
|
||||
QString generateUniqueFilePath(const QString& baseDir, const QString& fileName) const;
|
||||
void sendNextFile();
|
||||
LocalSend::RegisterDto buildRegisterDto() const;
|
||||
void resetSendState();
|
||||
void resetReceiveState();
|
||||
|
||||
bool m_manualSendPending = false;
|
||||
LocalSend::Device m_manualSendDevice;
|
||||
};
|
||||
|
||||
@@ -4,6 +4,10 @@ qt_add_executable(LocalSendQt
|
||||
AppController.cpp
|
||||
)
|
||||
|
||||
set_target_properties(LocalSendQt PROPERTIES
|
||||
OUTPUT_NAME localsend-qt
|
||||
)
|
||||
|
||||
qt_add_qml_module(LocalSendQt
|
||||
URI LocalSend
|
||||
VERSION 1.0
|
||||
@@ -18,6 +22,7 @@ target_link_libraries(LocalSendQt PRIVATE
|
||||
)
|
||||
|
||||
install(TARGETS LocalSendQt
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
BUNDLE DESTINATION .
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
)
|
||||
|
||||
@@ -14,7 +14,74 @@ ApplicationWindow {
|
||||
property var currentFiles: []
|
||||
property string currentSenderAlias: ""
|
||||
property string currentSenderIp: ""
|
||||
property var receiveProgress: ({})
|
||||
|
||||
function getDeviceTypeIcon(deviceType) {
|
||||
switch (deviceType) {
|
||||
case "mobile": return "\u{1F4F1}"
|
||||
case "desktop": return "\u{1F4BB}"
|
||||
case "web": return "\u{1F310}"
|
||||
case "headless": return "\u{1F5A5}"
|
||||
case "server": return "\u{1F4E1}"
|
||||
default: return "\u{1F4BB}"
|
||||
}
|
||||
}
|
||||
|
||||
DropArea {
|
||||
id: dropArea
|
||||
anchors.fill: parent
|
||||
z: 9999
|
||||
|
||||
onEntered: function(drag) {
|
||||
if (drag.hasUrls) {
|
||||
drag.accepted = true
|
||||
dropOverlay.visible = true
|
||||
} else {
|
||||
drag.accepted = false
|
||||
}
|
||||
}
|
||||
|
||||
onDropped: function(drop) {
|
||||
dropOverlay.visible = false
|
||||
if (drop.hasUrls) {
|
||||
var paths = []
|
||||
for (var i = 0; i < drop.urls.length; i++) {
|
||||
paths.push(drop.urls[i].toString())
|
||||
}
|
||||
if (paths.length > 0) {
|
||||
appController.addFiles(paths)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
dropOverlay.visible = false
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dropOverlay
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
color: "#200080FF"
|
||||
z: 10000
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("Drop files here to add them")
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
color: "#0080FF"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
color: "transparent"
|
||||
radius: 12
|
||||
border.color: "#0080FF"
|
||||
border.width: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: stackView
|
||||
@@ -25,6 +92,21 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
title: qsTr("Select Files")
|
||||
fileMode: FileDialog.OpenFiles
|
||||
onAccepted: {
|
||||
var paths = []
|
||||
for (var i = 0; i < selectedFiles.length; i++) {
|
||||
paths.push(selectedFiles[i].toString())
|
||||
}
|
||||
if (paths.length > 0) {
|
||||
appController.addFiles(paths)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: receiveDialog
|
||||
anchors.centerIn: parent
|
||||
@@ -35,7 +117,7 @@ ApplicationWindow {
|
||||
|
||||
onAccepted: {
|
||||
appController.acceptReceive(currentSessionId)
|
||||
currentSessionId = ""
|
||||
receiveProgressDialog.open()
|
||||
}
|
||||
|
||||
onRejected: {
|
||||
@@ -73,7 +155,7 @@ ApplicationWindow {
|
||||
}
|
||||
Label {
|
||||
text: formatSize(modelData.size)
|
||||
color: "gray"
|
||||
color: palette.mid
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +169,7 @@ ApplicationWindow {
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: progressDialog
|
||||
id: receiveProgressDialog
|
||||
anchors.centerIn: parent
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
@@ -97,25 +179,88 @@ ApplicationWindow {
|
||||
spacing: 12
|
||||
|
||||
Label {
|
||||
id: progressLabel
|
||||
text: qsTr("Receiving from %1...").arg(currentSenderAlias)
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
id: totalProgressBar
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 100
|
||||
value: calculateTotalProgress()
|
||||
text: qsTr("From: %1").arg(appController.currentReceiveSenderAlias)
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("%1% complete").arg(Math.round(totalProgressBar.value))
|
||||
color: "gray"
|
||||
text: appController.currentReceiveFileName
|
||||
? qsTr("%1 (%2/%3)").arg(appController.currentReceiveFileName)
|
||||
.arg(appController.currentReceiveFileIndex)
|
||||
.arg(appController.totalReceiveFiles)
|
||||
: qsTr("Waiting for data...")
|
||||
elide: Text.ElideMiddle
|
||||
Layout.maximumWidth: 400
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 100
|
||||
value: appController.receiveProgress
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("%1% complete").arg(Math.round(appController.receiveProgress))
|
||||
color: palette.mid
|
||||
}
|
||||
}
|
||||
|
||||
property var progressData: ({})
|
||||
footer: DialogButtonBox {
|
||||
Button {
|
||||
text: qsTr("Cancel")
|
||||
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
|
||||
onClicked: {
|
||||
appController.cancelReceive()
|
||||
receiveProgressDialog.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: sendProgressDialog
|
||||
anchors.centerIn: parent
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
title: qsTr("Sending Files")
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 12
|
||||
|
||||
Label {
|
||||
text: appController.currentSendFileName
|
||||
? qsTr("%1 (%2/%3)").arg(appController.currentSendFileName)
|
||||
.arg(appController.currentSendFileIndex)
|
||||
.arg(appController.totalSendFiles)
|
||||
: qsTr("Preparing...")
|
||||
elide: Text.ElideMiddle
|
||||
Layout.maximumWidth: 400
|
||||
}
|
||||
|
||||
ProgressBar {
|
||||
Layout.fillWidth: true
|
||||
from: 0
|
||||
to: 100
|
||||
value: appController.sendProgress
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("%1% complete").arg(Math.round(appController.sendProgress))
|
||||
color: palette.mid
|
||||
}
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
Button {
|
||||
text: qsTr("Cancel")
|
||||
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
|
||||
onClicked: {
|
||||
appController.cancelSend()
|
||||
sendProgressDialog.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -129,33 +274,50 @@ ApplicationWindow {
|
||||
|
||||
if (appController.quickSave) {
|
||||
appController.acceptReceive(sessionId)
|
||||
receiveProgressDialog.open()
|
||||
} else {
|
||||
receiveDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
function onReceiveProgress(sessionId, fileId, progress) {
|
||||
if (sessionId === currentSessionId) {
|
||||
receiveProgress[fileId] = progress
|
||||
receiveProgress = Object.assign({}, receiveProgress)
|
||||
progressDialog.progressData = receiveProgress
|
||||
}
|
||||
}
|
||||
|
||||
function onReceiveCompleted(sessionId) {
|
||||
if (sessionId === currentSessionId) {
|
||||
progressDialog.close()
|
||||
receiveProgress = {}
|
||||
currentSessionId = ""
|
||||
}
|
||||
receiveProgressDialog.close()
|
||||
}
|
||||
|
||||
function onReceiveError(sessionId, error) {
|
||||
if (sessionId === currentSessionId) {
|
||||
progressDialog.close()
|
||||
receiveProgressDialog.close()
|
||||
errorDialog.text = error
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
function onSendingChanged() {
|
||||
if (appController.sending && !sendProgressDialog.visible) {
|
||||
sendProgressDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
function onSendCompleted(sessionId) {
|
||||
sendProgressDialog.close()
|
||||
successDialog.text = qsTr("Files sent successfully!")
|
||||
successDialog.open()
|
||||
}
|
||||
|
||||
function onSendError(error) {
|
||||
sendProgressDialog.close()
|
||||
pinDialog.close()
|
||||
errorDialog.text = error
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
function onSendCanceled() {
|
||||
sendProgressDialog.close()
|
||||
errorDialog.text = qsTr("Transfer canceled by receiver")
|
||||
errorDialog.open()
|
||||
}
|
||||
|
||||
function onPinRequired(firstAttempt) {
|
||||
pinDialog.isFirstAttempt = firstAttempt
|
||||
pinDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +334,171 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: successDialog
|
||||
anchors.centerIn: parent
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok
|
||||
title: qsTr("Success")
|
||||
property alias text: successLabel.text
|
||||
|
||||
Label {
|
||||
id: successLabel
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: pinDialog
|
||||
anchors.centerIn: parent
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
title: qsTr("PIN Required")
|
||||
|
||||
property bool isFirstAttempt: true
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 12
|
||||
|
||||
Label {
|
||||
text: pinDialog.isFirstAttempt
|
||||
? qsTr("The receiver requires a PIN to accept files.")
|
||||
: qsTr("Invalid PIN. Please try again.")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
color: pinDialog.isFirstAttempt ? palette.text : "red"
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: pinInput
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Enter PIN")
|
||||
echoMode: TextInput.Password
|
||||
focus: true
|
||||
onAccepted: pinDialog.submitPin()
|
||||
}
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
Button {
|
||||
text: qsTr("Cancel")
|
||||
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
|
||||
}
|
||||
Button {
|
||||
text: qsTr("Submit")
|
||||
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
|
||||
enabled: pinInput.text.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: submitPin()
|
||||
onRejected: {
|
||||
appController.cancelSend()
|
||||
sendProgressDialog.close()
|
||||
close()
|
||||
}
|
||||
|
||||
function submitPin() {
|
||||
if (pinInput.text.length > 0) {
|
||||
appController.retryWithPin(pinInput.text)
|
||||
pinInput.text = ""
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
pinInput.text = ""
|
||||
pinInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: manualSendDialog
|
||||
anchors.centerIn: parent
|
||||
modal: true
|
||||
title: qsTr("Manual Sending")
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 12
|
||||
|
||||
Label {
|
||||
text: qsTr("Enter the IP address of the target device.")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: manualAddressInput
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("IP address (e.g., 192.168.1.100:53317)")
|
||||
onAccepted: manualSendDialog.accepted()
|
||||
}
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
Button {
|
||||
text: qsTr("Cancel")
|
||||
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
|
||||
}
|
||||
Button {
|
||||
text: qsTr("Send")
|
||||
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
|
||||
enabled: manualAddressInput.text.trim().length > 0
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
appController.sendToAddress(manualAddressInput.text.trim())
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
manualAddressInput.text = ""
|
||||
manualAddressInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: setPinDialog
|
||||
anchors.centerIn: parent
|
||||
modal: true
|
||||
title: qsTr("Set Receive PIN")
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 12
|
||||
|
||||
Label {
|
||||
text: qsTr("Enter a PIN that senders must provide to transfer files to this device. Leave empty to disable.")
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: setPinInput
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("Enter PIN")
|
||||
onAccepted: setPinDialog.accepted()
|
||||
}
|
||||
}
|
||||
|
||||
footer: DialogButtonBox {
|
||||
Button {
|
||||
text: qsTr("Cancel")
|
||||
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
|
||||
}
|
||||
Button {
|
||||
text: qsTr("OK")
|
||||
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
|
||||
}
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
appController.receivePin = setPinInput.text
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
setPinInput.text = appController.receivePin
|
||||
setPinInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes < 1024) return bytes + " B"
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
|
||||
@@ -179,20 +506,6 @@ ApplicationWindow {
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB"
|
||||
}
|
||||
|
||||
function calculateTotalProgress() {
|
||||
if (!currentFiles || currentFiles.length === 0) return 0
|
||||
var total = 0
|
||||
var count = 0
|
||||
for (var i = 0; i < currentFiles.length; i++) {
|
||||
var fileId = currentFiles[i].id
|
||||
if (receiveProgress[fileId] !== undefined) {
|
||||
total += receiveProgress[fileId] * 100
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count > 0 ? total / currentFiles.length : 0
|
||||
}
|
||||
|
||||
Component {
|
||||
id: homePageComponent
|
||||
Page {
|
||||
@@ -222,6 +535,86 @@ ApplicationWindow {
|
||||
anchors.margins: 16
|
||||
spacing: 16
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Label {
|
||||
text: qsTr("Selected Files")
|
||||
font.bold: true
|
||||
font.pixelSize: 16
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
text: qsTr("Add Files")
|
||||
onClicked: fileDialog.open()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Clear All")
|
||||
visible: appController.hasPendingFiles
|
||||
onClicked: appController.clearPendingFiles()
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: pendingFilesList
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.min(180, contentHeight)
|
||||
model: appController.pendingFiles
|
||||
spacing: 4
|
||||
visible: appController.hasPendingFiles
|
||||
clip: true
|
||||
|
||||
delegate: Pane {
|
||||
width: ListView.view.width
|
||||
padding: 8
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
radius: 6
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: modelData.fileName
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
Label {
|
||||
text: formatSize(modelData.size)
|
||||
color: palette.mid
|
||||
font.pixelSize: 12
|
||||
}
|
||||
ToolButton {
|
||||
text: "\u2715"
|
||||
font.pixelSize: 14
|
||||
onClicked: appController.removePendingFile(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
visible: !appController.hasPendingFiles
|
||||
text: qsTr("No files selected. Click \"Add Files\" or drag and drop files here.")
|
||||
color: palette.mid
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: qsTr("Nearby Devices")
|
||||
font.bold: true
|
||||
@@ -242,42 +635,67 @@ ApplicationWindow {
|
||||
padding: 12
|
||||
|
||||
background: Rectangle {
|
||||
color: Qt.lighter("gray", 1.8)
|
||||
color: "transparent"
|
||||
radius: 8
|
||||
border.color: palette.mid
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 12
|
||||
|
||||
Label {
|
||||
text: getDeviceTypeIcon(modelData.deviceType)
|
||||
font.pixelSize: 32
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: modelData.alias || modelData.ip
|
||||
font.bold: true
|
||||
color: palette.text
|
||||
}
|
||||
Label {
|
||||
text: "%1:%2".arg(modelData.ip).arg(modelData.port)
|
||||
color: "gray"
|
||||
color: palette.mid
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
visible: appController.hasPendingFiles && !appController.sending
|
||||
text: qsTr("Send")
|
||||
enabled: !appController.sending
|
||||
onClicked: {
|
||||
// TODO: send to this device
|
||||
if (appController.hasPendingFiles && !appController.sending) {
|
||||
appController.sendTo(modelData.fingerprint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("No devices found")
|
||||
color: palette.mid
|
||||
visible: deviceListView.count === 0
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Button {
|
||||
text: qsTr("Refresh")
|
||||
onClicked: appController.refreshDevices()
|
||||
}
|
||||
Button {
|
||||
text: qsTr("Manual Send")
|
||||
enabled: appController.hasPendingFiles && !appController.sending
|
||||
onClicked: manualSendDialog.open()
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Label {
|
||||
text: qsTr("Alias: %1").arg(appController.alias)
|
||||
@@ -317,6 +735,16 @@ ApplicationWindow {
|
||||
anchors.margins: 16
|
||||
spacing: 16
|
||||
|
||||
ScrollView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
contentWidth: availableWidth
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: 16
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
Layout.fillWidth: true
|
||||
@@ -359,6 +787,74 @@ ApplicationWindow {
|
||||
checked: appController.quickSave
|
||||
onCheckedChanged: appController.quickSave = checked
|
||||
}
|
||||
|
||||
Label { text: qsTr("Receive PIN:") }
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: appController.receivePin.length > 0 ? qsTr("Enabled") : qsTr("Disabled")
|
||||
color: appController.receivePin.length > 0 ? "green" : palette.mid
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
Button {
|
||||
text: appController.receivePin.length > 0 ? qsTr("Change") : qsTr("Set PIN")
|
||||
onClicked: setPinDialog.open()
|
||||
}
|
||||
Button {
|
||||
text: qsTr("Remove")
|
||||
visible: appController.receivePin.length > 0
|
||||
onClicked: appController.receivePin = ""
|
||||
}
|
||||
}
|
||||
|
||||
Label { text: qsTr("Device Type:") }
|
||||
ComboBox {
|
||||
id: deviceTypeCombo
|
||||
Layout.fillWidth: true
|
||||
model: [
|
||||
{ text: qsTr("Mobile"), value: "mobile" },
|
||||
{ text: qsTr("Desktop"), value: "desktop" },
|
||||
{ text: qsTr("Web"), value: "web" },
|
||||
{ text: qsTr("Headless"), value: "headless" },
|
||||
{ text: qsTr("Server"), value: "server" }
|
||||
]
|
||||
textRole: "text"
|
||||
Component.onCompleted: {
|
||||
for (var i = 0; i < model.length; i++) {
|
||||
if (model[i].value === appController.deviceType) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
onCurrentIndexChanged: {
|
||||
if (currentIndex >= 0 && currentIndex < model.length) {
|
||||
appController.deviceType = model[currentIndex].value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: advancedCheck
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Advanced Options")
|
||||
checked: false
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
Layout.fillWidth: true
|
||||
visible: advancedCheck.checked
|
||||
|
||||
Label { text: qsTr("HTTPS:") }
|
||||
CheckBox {
|
||||
id: httpsCheck
|
||||
checked: appController.https
|
||||
onCheckedChanged: appController.https = checked
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FolderDialog {
|
||||
@@ -369,9 +865,8 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: qsTr("Server Status: %1").arg(appController.serverRunning ? qsTr("Running") : qsTr("Stopped"))
|
||||
color: appController.serverRunning ? "green" : "red"
|
||||
}
|
||||
|
||||
@@ -53,3 +53,13 @@ set_target_properties(LocalSendCore PROPERTIES
|
||||
SOVERSION 1
|
||||
OUTPUT_NAME localsend-core
|
||||
)
|
||||
|
||||
install(TARGETS LocalSendCore
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
)
|
||||
|
||||
if(INSTALL_DEVEL_HEADERS)
|
||||
install(DIRECTORY include/LocalSendCore
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -22,6 +22,12 @@ namespace ApiRoute {
|
||||
constexpr const char* UPLOAD = "/api/localsend/v2/upload";
|
||||
constexpr const char* CANCEL = "/api/localsend/v2/cancel";
|
||||
constexpr const char* SHOW = "/api/localsend/v2/show";
|
||||
|
||||
constexpr const char* V1_INFO = "/api/localsend/v1/info";
|
||||
constexpr const char* V1_REGISTER = "/api/localsend/v1/register";
|
||||
constexpr const char* V1_PREPARE_UPLOAD = "/api/localsend/v1/send-request";
|
||||
constexpr const char* V1_UPLOAD = "/api/localsend/v1/send";
|
||||
constexpr const char* V1_CANCEL = "/api/localsend/v1/cancel";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,18 +22,21 @@ public:
|
||||
|
||||
void getInfo(const Device& device);
|
||||
void registerDevice(const Device& device, const RegisterDto& dto);
|
||||
void prepareUpload(const Device& device, const PrepareUploadRequestDto& dto);
|
||||
void prepareUpload(const Device& device, const PrepareUploadRequestDto& dto, const QString& pin = QString());
|
||||
void uploadFile(const Device& device, const QString& sessionId, const QString& fileId,
|
||||
const QString& token, const QString& filePath);
|
||||
void cancel(const Device& device, const QString& sessionId);
|
||||
void abortCurrentUpload();
|
||||
|
||||
signals:
|
||||
void infoReceived(const InfoDto& info);
|
||||
void infoError(const QString& error);
|
||||
void registerCompleted();
|
||||
void registerCompleted(const InfoDto& peerInfo);
|
||||
void registerError(const QString& error);
|
||||
void prepareUploadResponse(const PrepareUploadResponseDto& response);
|
||||
void prepareUploadError(const QString& error);
|
||||
void prepareUploadPinRequired();
|
||||
void prepareUploadTooManyAttempts();
|
||||
void uploadProgress(qint64 sent, qint64 total);
|
||||
void uploadCompleted();
|
||||
void uploadError(const QString& error);
|
||||
@@ -41,6 +44,7 @@ signals:
|
||||
private:
|
||||
QNetworkAccessManager* m_manager = nullptr;
|
||||
QSslConfiguration m_sslConfig;
|
||||
QNetworkReply* m_currentUploadReply = nullptr;
|
||||
|
||||
QNetworkReply* sendGet(const QUrl& url);
|
||||
QNetworkReply* sendPost(const QUrl& url, const QByteArray& data);
|
||||
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
void stop();
|
||||
|
||||
void setLocalInfo(const InfoDto& info, const QString& fingerprint);
|
||||
void setReceivePin(const QString& pin);
|
||||
#ifdef HAS_QTHTTPSERVER
|
||||
void setSslConfiguration(const QSslConfiguration& config);
|
||||
#endif
|
||||
@@ -39,6 +40,9 @@ public:
|
||||
void respondToPrepareUpload(const QString& sessionId, bool accepted,
|
||||
const QMap<QString, QString>& tokens = {});
|
||||
|
||||
void addActiveSession(const QString& sessionId);
|
||||
void removeActiveSession(const QString& sessionId);
|
||||
|
||||
signals:
|
||||
void registerRequest(const RegisterDto& dto, const QHostAddress& sender);
|
||||
void prepareUploadRequest(const QString& sessionId, const PrepareUploadRequestDto& dto,
|
||||
@@ -58,10 +62,13 @@ private:
|
||||
std::shared_ptr<QPromise<QHttpServerResponse>> promise;
|
||||
};
|
||||
QMap<QString, PendingPrepareUpload> m_pendingPrepareUploads;
|
||||
QSet<QString> m_activeSessions;
|
||||
#endif
|
||||
|
||||
InfoDto m_localInfo;
|
||||
QString m_localFingerprint;
|
||||
QString m_receivePin;
|
||||
QMap<QString, int> m_pinAttempts;
|
||||
quint16 m_port = 0;
|
||||
|
||||
#ifdef HAS_QTHTTPSERVER
|
||||
@@ -71,6 +78,7 @@ private:
|
||||
QFuture<QHttpServerResponse> handlePrepareUploadRequest(const QHttpServerRequest& request);
|
||||
QHttpServerResponse handleUploadRequest(const QHttpServerRequest& request);
|
||||
QHttpServerResponse handleCancelRequest(const QHttpServerRequest& request);
|
||||
bool checkPin(const QHttpServerRequest& request, const QHostAddress& peer);
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ public:
|
||||
|
||||
QString createSendSession(const Device& target, const QMap<QString, FileDto>& files,
|
||||
const QMap<QString, QString>& localPaths);
|
||||
void setSendSessionTokens(const QString& sessionId, const QString& responseSessionId,
|
||||
const QMap<QString, QString>& tokens);
|
||||
void startSendSession(const QString& sessionId);
|
||||
void updateSendProgress(const QString& sessionId, const QString& fileId, qint64 bytes);
|
||||
void completeSendFile(const QString& sessionId, const QString& fileId);
|
||||
|
||||
@@ -29,6 +29,9 @@ public:
|
||||
bool quickSave() const;
|
||||
void setQuickSave(bool enabled);
|
||||
|
||||
QString receivePin() const;
|
||||
void setReceivePin(const QString& pin);
|
||||
|
||||
QString deviceModel() const;
|
||||
void setDeviceModel(const QString& model);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <QFile>
|
||||
#include <QUrlQuery>
|
||||
#include <QJsonDocument>
|
||||
#include <QDebug>
|
||||
|
||||
namespace LocalSend {
|
||||
|
||||
@@ -10,6 +11,10 @@ HttpClient::HttpClient(QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_manager(new QNetworkAccessManager(this))
|
||||
{
|
||||
connect(m_manager, &QNetworkAccessManager::sslErrors,
|
||||
this, [](QNetworkReply* reply, const QList<QSslError>&) {
|
||||
reply->ignoreSslErrors();
|
||||
});
|
||||
}
|
||||
|
||||
HttpClient::~HttpClient()
|
||||
@@ -90,28 +95,66 @@ void HttpClient::registerDevice(const Device& device, const RegisterDto& dto)
|
||||
return;
|
||||
}
|
||||
|
||||
emit registerCompleted();
|
||||
QByteArray responseData = reply->readAll();
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseData, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
emit registerError(QStringLiteral("Invalid JSON response"));
|
||||
return;
|
||||
}
|
||||
|
||||
InfoDto info = InfoDto::fromJson(doc.object());
|
||||
emit registerCompleted(info);
|
||||
});
|
||||
}
|
||||
|
||||
void HttpClient::prepareUpload(const Device& device, const PrepareUploadRequestDto& dto)
|
||||
void HttpClient::prepareUpload(const Device& device, const PrepareUploadRequestDto& dto, const QString& pin)
|
||||
{
|
||||
QUrl url = buildUrl(device, ApiRoute::PREPARE_UPLOAD);
|
||||
if (!pin.isEmpty()) {
|
||||
QUrlQuery query;
|
||||
query.addQueryItem(QStringLiteral("pin"), pin);
|
||||
url.setQuery(query);
|
||||
}
|
||||
QByteArray data = QJsonDocument(dto.toJson()).toJson(QJsonDocument::Compact);
|
||||
|
||||
qDebug() << "[HttpClient] prepareUpload to" << url.toString();
|
||||
qDebug() << "[HttpClient] request:" << data;
|
||||
|
||||
QNetworkReply* reply = sendPost(url, data);
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
|
||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
if (statusCode == 401) {
|
||||
qDebug() << "[HttpClient] prepareUpload: PIN required (401)";
|
||||
emit prepareUploadPinRequired();
|
||||
return;
|
||||
}
|
||||
|
||||
if (statusCode == 429) {
|
||||
qWarning() << "[HttpClient] prepareUpload: Too many attempts (429)";
|
||||
emit prepareUploadTooManyAttempts();
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
qWarning() << "[HttpClient] prepareUpload error:" << reply->errorString();
|
||||
emit prepareUploadError(reply->errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray responseData = reply->readAll();
|
||||
qDebug() << "[HttpClient] prepareUpload response:" << responseData;
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(reply->readAll(), &error);
|
||||
QJsonDocument doc = QJsonDocument::fromJson(responseData, &error);
|
||||
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
qWarning() << "[HttpClient] Invalid JSON response";
|
||||
emit prepareUploadError(QStringLiteral("Invalid JSON response"));
|
||||
return;
|
||||
}
|
||||
@@ -131,14 +174,18 @@ void HttpClient::uploadFile(const Device& device, const QString& sessionId,
|
||||
query.addQueryItem(QStringLiteral("token"), token);
|
||||
url.setQuery(query);
|
||||
|
||||
qDebug() << "[HttpClient] uploadFile to" << url.toString();
|
||||
|
||||
QFile* file = new QFile(filePath);
|
||||
if (!file->open(QIODevice::ReadOnly)) {
|
||||
qWarning() << "[HttpClient] Cannot open file:" << filePath << file->errorString();
|
||||
emit uploadError(QStringLiteral("Cannot open file: ") + file->errorString());
|
||||
delete file;
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 fileSize = file->size();
|
||||
qDebug() << "[HttpClient] File size:" << fileSize;
|
||||
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/octet-stream"));
|
||||
@@ -148,20 +195,30 @@ void HttpClient::uploadFile(const Device& device, const QString& sessionId,
|
||||
}
|
||||
|
||||
QNetworkReply* reply = m_manager->post(request, file);
|
||||
m_currentUploadReply = reply;
|
||||
file->setParent(reply);
|
||||
|
||||
connect(reply, &QNetworkReply::uploadProgress, this, [this, fileSize](qint64 sent, qint64) {
|
||||
emit uploadProgress(sent, fileSize);
|
||||
});
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, filePath]() {
|
||||
m_currentUploadReply = nullptr;
|
||||
reply->deleteLater();
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (statusCode == 410) {
|
||||
qWarning() << "[HttpClient] uploadFile: session cancelled (410)";
|
||||
emit uploadError(QStringLiteral("SESSION_CANCELLED"));
|
||||
return;
|
||||
}
|
||||
qWarning() << "[HttpClient] uploadFile error:" << reply->errorString();
|
||||
emit uploadError(reply->errorString());
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "[HttpClient] uploadFile completed:" << filePath;
|
||||
emit uploadCompleted();
|
||||
});
|
||||
}
|
||||
@@ -178,5 +235,13 @@ void HttpClient::cancel(const Device& device, const QString& sessionId)
|
||||
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
|
||||
}
|
||||
|
||||
void HttpClient::abortCurrentUpload()
|
||||
{
|
||||
if (m_currentUploadReply) {
|
||||
m_currentUploadReply->abort();
|
||||
m_currentUploadReply = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#ifdef HAS_QTHTTPSERVER
|
||||
#include <QTcpServer>
|
||||
#include <QSslServer>
|
||||
#include <QJsonDocument>
|
||||
#include <QEventLoop>
|
||||
#include <QUuid>
|
||||
@@ -33,16 +34,67 @@ void HttpServer::setSslConfiguration(const QSslConfiguration& config)
|
||||
m_sslConfig = config;
|
||||
}
|
||||
|
||||
void HttpServer::setReceivePin(const QString& pin)
|
||||
{
|
||||
m_receivePin = pin;
|
||||
m_pinAttempts.clear();
|
||||
}
|
||||
|
||||
void HttpServer::addActiveSession(const QString& sessionId)
|
||||
{
|
||||
m_activeSessions.insert(sessionId);
|
||||
}
|
||||
|
||||
void HttpServer::removeActiveSession(const QString& sessionId)
|
||||
{
|
||||
m_activeSessions.remove(sessionId);
|
||||
}
|
||||
|
||||
bool HttpServer::checkPin(const QHttpServerRequest& request, const QHostAddress& peer)
|
||||
{
|
||||
if (m_receivePin.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
QString peerIp = peer.toString();
|
||||
int attempts = m_pinAttempts.value(peerIp, 0);
|
||||
|
||||
if (attempts >= 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QUrlQuery query(request.url().query());
|
||||
QString requestPin = query.queryItemValue(QStringLiteral("pin"));
|
||||
|
||||
if (requestPin != m_receivePin) {
|
||||
if (!requestPin.isEmpty()) {
|
||||
m_pinAttempts[peerIp] = attempts + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HttpServer::start(quint16 port, bool https)
|
||||
{
|
||||
Q_UNUSED(https)
|
||||
|
||||
if (m_tcpServer && m_tcpServer->isListening()) {
|
||||
stop();
|
||||
}
|
||||
|
||||
m_port = port;
|
||||
|
||||
if (https && !m_sslConfig.isNull()) {
|
||||
auto* sslServer = new QSslServer(this);
|
||||
sslServer->setSslConfiguration(m_sslConfig);
|
||||
if (!sslServer->listen(QHostAddress::Any, port)) {
|
||||
delete sslServer;
|
||||
return false;
|
||||
}
|
||||
m_tcpServer = sslServer;
|
||||
m_server->bind(m_tcpServer);
|
||||
qDebug() << "[HttpServer] Listening on HTTPS port:" << port;
|
||||
} else {
|
||||
m_tcpServer = new QTcpServer(this);
|
||||
if (!m_tcpServer->listen(QHostAddress::Any, port)) {
|
||||
delete m_tcpServer;
|
||||
@@ -50,6 +102,8 @@ bool HttpServer::start(quint16 port, bool https)
|
||||
return false;
|
||||
}
|
||||
m_server->bind(m_tcpServer);
|
||||
qDebug() << "[HttpServer] Listening on HTTP port:" << port;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -123,30 +177,33 @@ void HttpServer::respondToPrepareUpload(const QString& sessionId, bool accepted,
|
||||
|
||||
void HttpServer::setupRoutes()
|
||||
{
|
||||
m_server->route(ApiRoute::INFO, QHttpServerRequest::Method::Get,
|
||||
[this](const QHttpServerRequest&) {
|
||||
auto infoHandler = [this](const QHttpServerRequest&) {
|
||||
return handleInfoRequest();
|
||||
});
|
||||
|
||||
m_server->route(ApiRoute::REGISTER, QHttpServerRequest::Method::Post,
|
||||
[this](const QHttpServerRequest& request) {
|
||||
};
|
||||
auto registerHandler = [this](const QHttpServerRequest& request) {
|
||||
return handleRegisterRequest(request, request.remoteAddress());
|
||||
});
|
||||
|
||||
m_server->route(ApiRoute::PREPARE_UPLOAD, QHttpServerRequest::Method::Post,
|
||||
[this](const QHttpServerRequest& request) {
|
||||
};
|
||||
auto prepareUploadHandler = [this](const QHttpServerRequest& request) {
|
||||
return handlePrepareUploadRequest(request);
|
||||
});
|
||||
|
||||
m_server->route(ApiRoute::UPLOAD, QHttpServerRequest::Method::Post,
|
||||
[this](const QHttpServerRequest& request) {
|
||||
};
|
||||
auto uploadHandler = [this](const QHttpServerRequest& request) {
|
||||
return handleUploadRequest(request);
|
||||
});
|
||||
|
||||
m_server->route(ApiRoute::CANCEL, QHttpServerRequest::Method::Post,
|
||||
[this](const QHttpServerRequest& request) {
|
||||
};
|
||||
auto cancelHandler = [this](const QHttpServerRequest& request) {
|
||||
return handleCancelRequest(request);
|
||||
});
|
||||
};
|
||||
|
||||
m_server->route(ApiRoute::INFO, QHttpServerRequest::Method::Get, infoHandler);
|
||||
m_server->route(ApiRoute::REGISTER, QHttpServerRequest::Method::Post, registerHandler);
|
||||
m_server->route(ApiRoute::PREPARE_UPLOAD, QHttpServerRequest::Method::Post, prepareUploadHandler);
|
||||
m_server->route(ApiRoute::UPLOAD, QHttpServerRequest::Method::Post, uploadHandler);
|
||||
m_server->route(ApiRoute::CANCEL, QHttpServerRequest::Method::Post, cancelHandler);
|
||||
|
||||
m_server->route(ApiRoute::V1_INFO, QHttpServerRequest::Method::Get, infoHandler);
|
||||
m_server->route(ApiRoute::V1_REGISTER, QHttpServerRequest::Method::Post, registerHandler);
|
||||
m_server->route(ApiRoute::V1_PREPARE_UPLOAD, QHttpServerRequest::Method::Post, prepareUploadHandler);
|
||||
m_server->route(ApiRoute::V1_UPLOAD, QHttpServerRequest::Method::Post, uploadHandler);
|
||||
m_server->route(ApiRoute::V1_CANCEL, QHttpServerRequest::Method::Post, cancelHandler);
|
||||
}
|
||||
|
||||
QHttpServerResponse HttpServer::handleInfoRequest()
|
||||
@@ -180,6 +237,41 @@ QHttpServerResponse HttpServer::handleRegisterRequest(const QHttpServerRequest&
|
||||
|
||||
QFuture<QHttpServerResponse> HttpServer::handlePrepareUploadRequest(const QHttpServerRequest& request)
|
||||
{
|
||||
QHostAddress peer = request.remoteAddress();
|
||||
|
||||
if (!m_receivePin.isEmpty()) {
|
||||
QString peerIp = peer.toString();
|
||||
int attempts = m_pinAttempts.value(peerIp, 0);
|
||||
|
||||
if (attempts >= 3) {
|
||||
auto promise = std::make_shared<QPromise<QHttpServerResponse>>();
|
||||
promise->start();
|
||||
QJsonObject errorObj;
|
||||
errorObj[QStringLiteral("message")] = QStringLiteral("Too many attempts.");
|
||||
promise->addResult(QHttpServerResponse(QJsonDocument(errorObj).toJson(QJsonDocument::Compact),
|
||||
QHttpServerResponse::StatusCode::Forbidden));
|
||||
promise->finish();
|
||||
return promise->future();
|
||||
}
|
||||
|
||||
QUrlQuery query(request.url().query());
|
||||
QString requestPin = query.queryItemValue(QStringLiteral("pin"));
|
||||
|
||||
if (requestPin != m_receivePin) {
|
||||
if (!requestPin.isEmpty()) {
|
||||
m_pinAttempts[peerIp] = attempts + 1;
|
||||
}
|
||||
auto promise = std::make_shared<QPromise<QHttpServerResponse>>();
|
||||
promise->start();
|
||||
QJsonObject errorObj;
|
||||
errorObj[QStringLiteral("message")] = QStringLiteral("Invalid pin.");
|
||||
promise->addResult(QHttpServerResponse(QJsonDocument(errorObj).toJson(QJsonDocument::Compact),
|
||||
QHttpServerResponse::StatusCode::Unauthorized));
|
||||
promise->finish();
|
||||
return promise->future();
|
||||
}
|
||||
}
|
||||
|
||||
QJsonParseError error;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(request.body(), &error);
|
||||
|
||||
@@ -222,6 +314,14 @@ QHttpServerResponse HttpServer::handleUploadRequest(const QHttpServerRequest& re
|
||||
return QHttpServerResponse(QHttpServerResponse::StatusCode::BadRequest);
|
||||
}
|
||||
|
||||
if (!m_activeSessions.contains(sessionId)) {
|
||||
QJsonObject errorObj;
|
||||
errorObj[QStringLiteral("error")] = QStringLiteral("session_not_found");
|
||||
errorObj[QStringLiteral("message")] = QStringLiteral("Session not found or has been cancelled");
|
||||
return QHttpServerResponse(QJsonDocument(errorObj).toJson(QJsonDocument::Compact),
|
||||
QHttpServerResponse::StatusCode::Gone);
|
||||
}
|
||||
|
||||
emit uploadRequest(sessionId, fileId, token, request.body());
|
||||
|
||||
return QHttpServerResponse(QHttpServerResponse::StatusCode::Ok);
|
||||
@@ -293,6 +393,16 @@ bool HttpServer::isRunning() const
|
||||
return false;
|
||||
}
|
||||
|
||||
void HttpServer::addActiveSession(const QString& sessionId)
|
||||
{
|
||||
Q_UNUSED(sessionId)
|
||||
}
|
||||
|
||||
void HttpServer::removeActiveSession(const QString& sessionId)
|
||||
{
|
||||
Q_UNUSED(sessionId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -156,6 +156,25 @@ QString SessionManager::createSendSession(const Device& target, const QMap<QStri
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
void SessionManager::setSendSessionTokens(const QString& sessionId, const QString& responseSessionId,
|
||||
const QMap<QString, QString>& tokens)
|
||||
{
|
||||
if (!m_sendSessions.contains(sessionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SendSession session = m_sendSessions.take(sessionId);
|
||||
session.sessionId = responseSessionId;
|
||||
|
||||
for (auto it = tokens.constBegin(); it != tokens.constEnd(); ++it) {
|
||||
if (session.files.contains(it.key())) {
|
||||
session.files[it.key()].token = it.value();
|
||||
}
|
||||
}
|
||||
|
||||
m_sendSessions.insert(responseSessionId, session);
|
||||
}
|
||||
|
||||
void SessionManager::startSendSession(const QString& sessionId)
|
||||
{
|
||||
if (!m_sendSessions.contains(sessionId)) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "LocalSendCore/Settings.h"
|
||||
#include "LocalSendCore/Constants.h"
|
||||
#include <QStandardPaths>
|
||||
#include <QSysInfo>
|
||||
|
||||
namespace LocalSend {
|
||||
|
||||
@@ -11,6 +12,18 @@ Settings::Settings(QObject* parent)
|
||||
{
|
||||
}
|
||||
|
||||
namespace {
|
||||
DeviceType detectDeviceType() {
|
||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
||||
return DeviceType::Mobile;
|
||||
#elif defined(Q_OS_LINUX) || defined(Q_OS_MACOS) || defined(Q_OS_WINDOWS)
|
||||
return DeviceType::Desktop;
|
||||
#else
|
||||
return DeviceType::Headless;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
QString Settings::alias() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("alias"),
|
||||
@@ -40,7 +53,7 @@ void Settings::setPort(quint16 port)
|
||||
|
||||
bool Settings::https() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("https"), false).toBool();
|
||||
return m_settings.value(QStringLiteral("https"), true).toBool();
|
||||
}
|
||||
|
||||
void Settings::setHttps(bool enabled)
|
||||
@@ -78,6 +91,23 @@ void Settings::setQuickSave(bool enabled)
|
||||
}
|
||||
}
|
||||
|
||||
QString Settings::receivePin() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("receivePin")).toString();
|
||||
}
|
||||
|
||||
void Settings::setReceivePin(const QString& pin)
|
||||
{
|
||||
if (receivePin() != pin) {
|
||||
if (pin.isEmpty()) {
|
||||
m_settings.remove(QStringLiteral("receivePin"));
|
||||
} else {
|
||||
m_settings.setValue(QStringLiteral("receivePin"), pin);
|
||||
}
|
||||
m_settings.sync();
|
||||
}
|
||||
}
|
||||
|
||||
QString Settings::deviceModel() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("deviceModel"),
|
||||
@@ -91,8 +121,11 @@ void Settings::setDeviceModel(const QString& model)
|
||||
|
||||
DeviceType Settings::deviceType() const
|
||||
{
|
||||
return deviceTypeFromString(m_settings.value(QStringLiteral("deviceType"),
|
||||
QStringLiteral("desktop")).toString());
|
||||
QVariant stored = m_settings.value(QStringLiteral("deviceType"));
|
||||
if (stored.isValid()) {
|
||||
return deviceTypeFromString(stored.toString());
|
||||
}
|
||||
return detectDeviceType();
|
||||
}
|
||||
|
||||
void Settings::setDeviceType(DeviceType type)
|
||||
|
||||
@@ -5,3 +5,19 @@ add_test(NAME TestDtoTypes COMMAND TestDtoTypes)
|
||||
add_executable(TestMulticastDiscovery core/TestMulticastDiscovery.cpp)
|
||||
target_link_libraries(TestMulticastDiscovery PRIVATE LocalSendCore Qt6::Test)
|
||||
add_test(NAME TestMulticastDiscovery COMMAND TestMulticastDiscovery)
|
||||
|
||||
add_executable(TestTypes core/TestTypes.cpp)
|
||||
target_link_libraries(TestTypes PRIVATE LocalSendCore Qt6::Test)
|
||||
add_test(NAME TestTypes COMMAND TestTypes)
|
||||
|
||||
add_executable(TestDevice core/TestDevice.cpp)
|
||||
target_link_libraries(TestDevice PRIVATE LocalSendCore Qt6::Test)
|
||||
add_test(NAME TestDevice COMMAND TestDevice)
|
||||
|
||||
add_executable(TestSessionManager core/TestSessionManager.cpp)
|
||||
target_link_libraries(TestSessionManager PRIVATE LocalSendCore Qt6::Test)
|
||||
add_test(NAME TestSessionManager COMMAND TestSessionManager)
|
||||
|
||||
add_executable(TestDtoTypesExtended core/TestDtoTypesExtended.cpp)
|
||||
target_link_libraries(TestDtoTypesExtended PRIVATE LocalSendCore Qt6::Test)
|
||||
add_test(NAME TestDtoTypesExtended COMMAND TestDtoTypesExtended)
|
||||
|
||||
89
tests/core/TestDevice.cpp
Normal file
89
tests/core/TestDevice.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
#include <QtTest>
|
||||
#include <LocalSendCore/Device.h>
|
||||
|
||||
class TestDevice : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void testDefaultConstructor();
|
||||
void testParameterizedConstructor();
|
||||
void testDisplayNameWithAlias();
|
||||
void testDisplayNameWithoutAlias();
|
||||
void testIsHttps();
|
||||
void testEqualityByFingerprint();
|
||||
void testInequality();
|
||||
};
|
||||
|
||||
void TestDevice::testDefaultConstructor()
|
||||
{
|
||||
LocalSend::Device device;
|
||||
QVERIFY(device.ip.isEmpty());
|
||||
QCOMPARE(device.port, quint16(53317));
|
||||
QCOMPARE(device.protocol, LocalSend::ProtocolType::Http);
|
||||
QVERIFY(device.alias.isEmpty());
|
||||
QVERIFY(device.fingerprint.isEmpty());
|
||||
QCOMPARE(device.deviceType, LocalSend::DeviceType::Desktop);
|
||||
QVERIFY(!device.download);
|
||||
QCOMPARE(device.discoveryMethod, LocalSend::DiscoveryMethod::Multicast);
|
||||
QVERIFY(!device.lastSeen.isValid());
|
||||
}
|
||||
|
||||
void TestDevice::testParameterizedConstructor()
|
||||
{
|
||||
LocalSend::Device device(QStringLiteral("192.168.1.100"), 8080);
|
||||
QCOMPARE(device.ip, QStringLiteral("192.168.1.100"));
|
||||
QCOMPARE(device.port, quint16(8080));
|
||||
}
|
||||
|
||||
void TestDevice::testDisplayNameWithAlias()
|
||||
{
|
||||
LocalSend::Device device;
|
||||
device.alias = QStringLiteral("My Laptop");
|
||||
QCOMPARE(device.displayName(), QStringLiteral("My Laptop"));
|
||||
}
|
||||
|
||||
void TestDevice::testDisplayNameWithoutAlias()
|
||||
{
|
||||
LocalSend::Device device;
|
||||
device.ip = QStringLiteral("192.168.1.50");
|
||||
QCOMPARE(device.displayName(), QStringLiteral("192.168.1.50"));
|
||||
}
|
||||
|
||||
void TestDevice::testIsHttps()
|
||||
{
|
||||
LocalSend::Device httpDevice;
|
||||
httpDevice.protocol = LocalSend::ProtocolType::Http;
|
||||
QVERIFY(!httpDevice.isHttps());
|
||||
|
||||
LocalSend::Device httpsDevice;
|
||||
httpsDevice.protocol = LocalSend::ProtocolType::Https;
|
||||
QVERIFY(httpsDevice.isHttps());
|
||||
}
|
||||
|
||||
void TestDevice::testEqualityByFingerprint()
|
||||
{
|
||||
LocalSend::Device a;
|
||||
a.fingerprint = QStringLiteral("fp123");
|
||||
a.ip = QStringLiteral("10.0.0.1");
|
||||
|
||||
LocalSend::Device b;
|
||||
b.fingerprint = QStringLiteral("fp123");
|
||||
b.ip = QStringLiteral("10.0.0.2");
|
||||
|
||||
QVERIFY(a == b);
|
||||
}
|
||||
|
||||
void TestDevice::testInequality()
|
||||
{
|
||||
LocalSend::Device a;
|
||||
a.fingerprint = QStringLiteral("fp1");
|
||||
|
||||
LocalSend::Device b;
|
||||
b.fingerprint = QStringLiteral("fp2");
|
||||
|
||||
QVERIFY(a != b);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestDevice)
|
||||
#include "TestDevice.moc"
|
||||
181
tests/core/TestDtoTypesExtended.cpp
Normal file
181
tests/core/TestDtoTypesExtended.cpp
Normal file
@@ -0,0 +1,181 @@
|
||||
#include <QtTest>
|
||||
#include <LocalSendCore/DtoTypes.h>
|
||||
#include <LocalSendCore/Constants.h>
|
||||
#include <QDateTime>
|
||||
|
||||
class TestDtoTypesExtended : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void testFileDtoMetadata();
|
||||
void testFileDtoRoundTripWithMetadata();
|
||||
void testFileDtoWithoutOptionalFields();
|
||||
void testPrepareUploadResponseDto();
|
||||
void testReceiveRequestResponseDto();
|
||||
void testMulticastDtoVersionDefault();
|
||||
void testFileDtoSizeLarge();
|
||||
void testPrepareUploadRequestDtoRoundTrip();
|
||||
};
|
||||
|
||||
void TestDtoTypesExtended::testFileDtoMetadata()
|
||||
{
|
||||
LocalSend::FileDto dto;
|
||||
dto.id = QStringLiteral("0");
|
||||
dto.fileName = QStringLiteral("test.txt");
|
||||
dto.size = 1024;
|
||||
dto.fileType = QStringLiteral("text/plain");
|
||||
dto.metadata = LocalSend::FileDto::Metadata{
|
||||
QDateTime::fromMSecsSinceEpoch(1700000000000),
|
||||
QDateTime::fromMSecsSinceEpoch(1700000001000)
|
||||
};
|
||||
|
||||
QJsonObject json = dto.toJson();
|
||||
QVERIFY(json.contains(QStringLiteral("metadata")));
|
||||
QJsonObject metaObj = json[QStringLiteral("metadata")].toObject();
|
||||
QCOMPARE(metaObj[QStringLiteral("lastModified")].toVariant().toLongLong(), qint64(1700000000000));
|
||||
QCOMPARE(metaObj[QStringLiteral("lastAccessed")].toVariant().toLongLong(), qint64(1700000001000));
|
||||
}
|
||||
|
||||
void TestDtoTypesExtended::testFileDtoRoundTripWithMetadata()
|
||||
{
|
||||
LocalSend::FileDto dto;
|
||||
dto.id = QStringLiteral("1");
|
||||
dto.fileName = QStringLiteral("doc.pdf");
|
||||
dto.size = 2048;
|
||||
dto.fileType = QStringLiteral("application/pdf");
|
||||
dto.hash = QStringLiteral("abc123");
|
||||
dto.preview = QStringLiteral("base64data");
|
||||
dto.metadata = LocalSend::FileDto::Metadata{
|
||||
QDateTime::fromMSecsSinceEpoch(1700000000000),
|
||||
QDateTime()
|
||||
};
|
||||
|
||||
QJsonObject json = dto.toJson();
|
||||
LocalSend::FileDto parsed = LocalSend::FileDto::fromJson(json);
|
||||
|
||||
QCOMPARE(parsed.id, dto.id);
|
||||
QCOMPARE(parsed.fileName, dto.fileName);
|
||||
QCOMPARE(parsed.size, dto.size);
|
||||
QCOMPARE(parsed.fileType, dto.fileType);
|
||||
QCOMPARE(parsed.hash, dto.hash);
|
||||
QCOMPARE(parsed.preview, dto.preview);
|
||||
QVERIFY(parsed.metadata.has_value());
|
||||
QCOMPARE(parsed.metadata->lastModified.toMSecsSinceEpoch(), qint64(1700000000000));
|
||||
QVERIFY(!parsed.metadata->lastAccessed.isValid());
|
||||
}
|
||||
|
||||
void TestDtoTypesExtended::testFileDtoWithoutOptionalFields()
|
||||
{
|
||||
LocalSend::FileDto dto;
|
||||
dto.id = QStringLiteral("2");
|
||||
dto.fileName = QStringLiteral("simple.txt");
|
||||
dto.size = 50;
|
||||
dto.fileType = QStringLiteral("text/plain");
|
||||
|
||||
QJsonObject json = dto.toJson();
|
||||
QVERIFY(!json.contains(QStringLiteral("hash")));
|
||||
QVERIFY(!json.contains(QStringLiteral("preview")));
|
||||
QVERIFY(!json.contains(QStringLiteral("metadata")));
|
||||
|
||||
LocalSend::FileDto parsed = LocalSend::FileDto::fromJson(json);
|
||||
QVERIFY(!parsed.metadata.has_value());
|
||||
QVERIFY(parsed.hash.isEmpty());
|
||||
QVERIFY(parsed.preview.isEmpty());
|
||||
}
|
||||
|
||||
void TestDtoTypesExtended::testPrepareUploadResponseDto()
|
||||
{
|
||||
QJsonObject json;
|
||||
json[QStringLiteral("sessionId")] = QStringLiteral("sess-123");
|
||||
QJsonObject filesObj;
|
||||
filesObj[QStringLiteral("0")] = QStringLiteral("token-0");
|
||||
filesObj[QStringLiteral("1")] = QStringLiteral("token-1");
|
||||
json[QStringLiteral("files")] = filesObj;
|
||||
|
||||
LocalSend::PrepareUploadResponseDto dto = LocalSend::PrepareUploadResponseDto::fromJson(json);
|
||||
|
||||
QCOMPARE(dto.sessionId, QStringLiteral("sess-123"));
|
||||
QCOMPARE(dto.files.size(), 2);
|
||||
QCOMPARE(dto.files[QStringLiteral("0")], QStringLiteral("token-0"));
|
||||
QCOMPARE(dto.files[QStringLiteral("1")], QStringLiteral("token-1"));
|
||||
}
|
||||
|
||||
void TestDtoTypesExtended::testReceiveRequestResponseDto()
|
||||
{
|
||||
LocalSend::ReceiveRequestResponseDto dto;
|
||||
dto.sessionId = QStringLiteral("sess-456");
|
||||
dto.cancel = false;
|
||||
dto.destinationPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/file0.txt"));
|
||||
dto.destinationPaths.insert(QStringLiteral("1"), QStringLiteral("/tmp/file1.txt"));
|
||||
|
||||
QJsonObject json = dto.toJson();
|
||||
QCOMPARE(json[QStringLiteral("sessionId")].toString(), QStringLiteral("sess-456"));
|
||||
QCOMPARE(json[QStringLiteral("cancel")].toBool(), false);
|
||||
|
||||
QJsonObject pathsObj = json[QStringLiteral("destinationPaths")].toObject();
|
||||
QCOMPARE(pathsObj.size(), 2);
|
||||
QCOMPARE(pathsObj[QStringLiteral("0")].toString(), QStringLiteral("/tmp/file0.txt"));
|
||||
}
|
||||
|
||||
void TestDtoTypesExtended::testMulticastDtoVersionDefault()
|
||||
{
|
||||
LocalSend::MulticastDto dto;
|
||||
dto.alias = QStringLiteral("Test");
|
||||
dto.fingerprint = QStringLiteral("fp1");
|
||||
|
||||
QJsonObject json = dto.toJson();
|
||||
QCOMPARE(json[QStringLiteral("version")].toString(), QString(LocalSend::PROTOCOL_VERSION));
|
||||
}
|
||||
|
||||
void TestDtoTypesExtended::testFileDtoSizeLarge()
|
||||
{
|
||||
LocalSend::FileDto dto;
|
||||
dto.id = QStringLiteral("big");
|
||||
dto.fileName = QStringLiteral("big.iso");
|
||||
dto.size = Q_INT64_C(4700000000);
|
||||
dto.fileType = QStringLiteral("application/x-iso9660-image");
|
||||
|
||||
QJsonObject json = dto.toJson();
|
||||
LocalSend::FileDto parsed = LocalSend::FileDto::fromJson(json);
|
||||
QCOMPARE(parsed.size, Q_INT64_C(4700000000));
|
||||
}
|
||||
|
||||
void TestDtoTypesExtended::testPrepareUploadRequestDtoRoundTrip()
|
||||
{
|
||||
LocalSend::PrepareUploadRequestDto dto;
|
||||
dto.info.alias = QStringLiteral("Alice");
|
||||
dto.info.fingerprint = QStringLiteral("fp-alice");
|
||||
dto.info.port = 53317;
|
||||
dto.info.protocol = LocalSend::ProtocolType::Https;
|
||||
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("photo.jpg");
|
||||
f1.size = 3000000;
|
||||
f1.fileType = QStringLiteral("image/jpeg");
|
||||
dto.files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
LocalSend::FileDto f2;
|
||||
f2.id = QStringLiteral("1");
|
||||
f2.fileName = QStringLiteral("notes.txt");
|
||||
f2.size = 500;
|
||||
f2.fileType = QStringLiteral("text/plain");
|
||||
f2.hash = QStringLiteral("sha256abc");
|
||||
dto.files.insert(QStringLiteral("1"), f2);
|
||||
|
||||
QJsonObject json = dto.toJson();
|
||||
LocalSend::PrepareUploadRequestDto parsed = LocalSend::PrepareUploadRequestDto::fromJson(json);
|
||||
|
||||
QCOMPARE(parsed.info.alias, QStringLiteral("Alice"));
|
||||
QCOMPARE(parsed.info.fingerprint, QStringLiteral("fp-alice"));
|
||||
QCOMPARE(parsed.info.port, quint16(53317));
|
||||
QCOMPARE(parsed.info.protocol, LocalSend::ProtocolType::Https);
|
||||
QCOMPARE(parsed.files.size(), 2);
|
||||
QCOMPARE(parsed.files[QStringLiteral("0")].fileName, QStringLiteral("photo.jpg"));
|
||||
QCOMPARE(parsed.files[QStringLiteral("0")].size, qint64(3000000));
|
||||
QCOMPARE(parsed.files[QStringLiteral("1")].hash, QStringLiteral("sha256abc"));
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestDtoTypesExtended)
|
||||
#include "TestDtoTypesExtended.moc"
|
||||
552
tests/core/TestSessionManager.cpp
Normal file
552
tests/core/TestSessionManager.cpp
Normal file
@@ -0,0 +1,552 @@
|
||||
#include <QtTest>
|
||||
#include <LocalSendCore/SessionManager.h>
|
||||
#include <LocalSendCore/Device.h>
|
||||
|
||||
class TestSessionManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void testCreateReceiveSession();
|
||||
void testAcceptReceiveSession();
|
||||
void testDeclineReceiveSession();
|
||||
void testCancelReceiveSession();
|
||||
void testReceiveProgress();
|
||||
void testCompleteReceiveFile();
|
||||
void testCompleteAllReceiveFiles();
|
||||
void testFailReceiveFile();
|
||||
void testCreateSendSession();
|
||||
void testSetSendSessionTokens();
|
||||
void testStartSendSession();
|
||||
void testSendProgress();
|
||||
void testCompleteSendFile();
|
||||
void testCompleteAllSendFiles();
|
||||
void testCancelSendSession();
|
||||
void testHasSessionQueries();
|
||||
void testGenerateToken();
|
||||
void testDeclineRemovesSession();
|
||||
void testSetSendSessionTokensRekeys();
|
||||
};
|
||||
|
||||
void TestSessionManager::testCreateReceiveSession()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device sender;
|
||||
sender.ip = QStringLiteral("10.0.0.1");
|
||||
sender.alias = QStringLiteral("Sender");
|
||||
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 100;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
LocalSend::FileDto f2;
|
||||
f2.id = QStringLiteral("1");
|
||||
f2.fileName = QStringLiteral("b.txt");
|
||||
f2.size = 200;
|
||||
files.insert(QStringLiteral("1"), f2);
|
||||
|
||||
QSignalSpy spy(&mgr, &LocalSend::SessionManager::receiveSessionCreated);
|
||||
QString sessionId = mgr.createReceiveSession(sender, files);
|
||||
|
||||
QVERIFY(!sessionId.isEmpty());
|
||||
QCOMPARE(spy.count(), 1);
|
||||
|
||||
LocalSend::ReceiveSession session = mgr.receiveSession(sessionId);
|
||||
QCOMPARE(session.sessionId, sessionId);
|
||||
QCOMPARE(session.sender.alias, QStringLiteral("Sender"));
|
||||
QCOMPARE(session.files.size(), 2);
|
||||
QCOMPARE(session.status, LocalSend::SessionStatus::Waiting);
|
||||
QVERIFY(!session.files[QStringLiteral("0")].token.isEmpty());
|
||||
QVERIFY(!session.files[QStringLiteral("1")].token.isEmpty());
|
||||
}
|
||||
|
||||
void TestSessionManager::testAcceptReceiveSession()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device sender;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 100;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QString sessionId = mgr.createReceiveSession(sender, files);
|
||||
|
||||
QSignalSpy spy(&mgr, &LocalSend::SessionManager::receiveSessionAccepted);
|
||||
QMap<QString, QString> destPaths;
|
||||
destPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/a.txt"));
|
||||
mgr.acceptReceiveSession(sessionId, destPaths);
|
||||
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QList<QVariant> args = spy.takeFirst();
|
||||
QCOMPARE(args.at(0).toString(), sessionId);
|
||||
|
||||
LocalSend::ReceiveSession session = mgr.receiveSession(sessionId);
|
||||
QCOMPARE(session.status, LocalSend::SessionStatus::Sending);
|
||||
QCOMPARE(session.files[QStringLiteral("0")].destinationPath, QStringLiteral("/tmp/a.txt"));
|
||||
|
||||
auto tokens = args.at(1).value<QMap<QString, QString>>();
|
||||
QVERIFY(!tokens.value(QStringLiteral("0")).isEmpty());
|
||||
}
|
||||
|
||||
void TestSessionManager::testDeclineReceiveSession()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device sender;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 100;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QString sessionId = mgr.createReceiveSession(sender, files);
|
||||
|
||||
QSignalSpy spy(&mgr, &LocalSend::SessionManager::receiveSessionDeclined);
|
||||
mgr.declineReceiveSession(sessionId);
|
||||
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QVERIFY(!mgr.hasReceiveSession(sessionId));
|
||||
}
|
||||
|
||||
void TestSessionManager::testCancelReceiveSession()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device sender;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 100;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QString sessionId = mgr.createReceiveSession(sender, files);
|
||||
QMap<QString, QString> destPaths;
|
||||
destPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/a.txt"));
|
||||
mgr.acceptReceiveSession(sessionId, destPaths);
|
||||
|
||||
QSignalSpy spy(&mgr, &LocalSend::SessionManager::receiveSessionCanceled);
|
||||
mgr.cancelReceiveSession(sessionId);
|
||||
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QVERIFY(!mgr.hasReceiveSession(sessionId));
|
||||
}
|
||||
|
||||
void TestSessionManager::testReceiveProgress()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device sender;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 1000;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QString sessionId = mgr.createReceiveSession(sender, files);
|
||||
QMap<QString, QString> destPaths;
|
||||
destPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/a.txt"));
|
||||
mgr.acceptReceiveSession(sessionId, destPaths);
|
||||
|
||||
QSignalSpy spy(&mgr, &LocalSend::SessionManager::receiveProgress);
|
||||
mgr.updateReceiveProgress(sessionId, QStringLiteral("0"), 500);
|
||||
|
||||
QCOMPARE(spy.count(), 1);
|
||||
double progress = spy.takeFirst().at(2).toDouble();
|
||||
QVERIFY(qFuzzyCompare(progress, 0.5));
|
||||
}
|
||||
|
||||
void TestSessionManager::testCompleteReceiveFile()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device sender;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 1000;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QString sessionId = mgr.createReceiveSession(sender, files);
|
||||
QMap<QString, QString> destPaths;
|
||||
destPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/a.txt"));
|
||||
mgr.acceptReceiveSession(sessionId, destPaths);
|
||||
|
||||
mgr.updateReceiveProgress(sessionId, QStringLiteral("0"), 1000);
|
||||
|
||||
QSignalSpy spy(&mgr, &LocalSend::SessionManager::receiveSessionCompleted);
|
||||
mgr.completeReceiveFile(sessionId, QStringLiteral("0"));
|
||||
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QVERIFY(!mgr.hasReceiveSession(sessionId));
|
||||
}
|
||||
|
||||
void TestSessionManager::testCompleteAllReceiveFiles()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device sender;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 100;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
LocalSend::FileDto f2;
|
||||
f2.id = QStringLiteral("1");
|
||||
f2.fileName = QStringLiteral("b.txt");
|
||||
f2.size = 200;
|
||||
files.insert(QStringLiteral("1"), f2);
|
||||
|
||||
QString sessionId = mgr.createReceiveSession(sender, files);
|
||||
QMap<QString, QString> destPaths;
|
||||
destPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/a.txt"));
|
||||
destPaths.insert(QStringLiteral("1"), QStringLiteral("/tmp/b.txt"));
|
||||
mgr.acceptReceiveSession(sessionId, destPaths);
|
||||
|
||||
QSignalSpy spy(&mgr, &LocalSend::SessionManager::receiveSessionCompleted);
|
||||
|
||||
mgr.completeReceiveFile(sessionId, QStringLiteral("0"));
|
||||
QCOMPARE(spy.count(), 0);
|
||||
QVERIFY(mgr.hasReceiveSession(sessionId));
|
||||
|
||||
mgr.completeReceiveFile(sessionId, QStringLiteral("1"));
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QVERIFY(!mgr.hasReceiveSession(sessionId));
|
||||
}
|
||||
|
||||
void TestSessionManager::testFailReceiveFile()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device sender;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 1000;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QString sessionId = mgr.createReceiveSession(sender, files);
|
||||
|
||||
mgr.failReceiveFile(sessionId, QStringLiteral("0"));
|
||||
|
||||
LocalSend::ReceiveSession session = mgr.receiveSession(sessionId);
|
||||
QCOMPARE(session.files[QStringLiteral("0")].status, LocalSend::FileStatus::Failed);
|
||||
}
|
||||
|
||||
void TestSessionManager::testCreateSendSession()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device target;
|
||||
target.ip = QStringLiteral("10.0.0.2");
|
||||
target.alias = QStringLiteral("Receiver");
|
||||
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("c.txt");
|
||||
f1.size = 500;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QMap<QString, QString> localPaths;
|
||||
localPaths.insert(QStringLiteral("0"), QStringLiteral("/home/user/c.txt"));
|
||||
|
||||
QSignalSpy spy(&mgr, &LocalSend::SessionManager::sendSessionCreated);
|
||||
QString sessionId = mgr.createSendSession(target, files, localPaths);
|
||||
|
||||
QVERIFY(!sessionId.isEmpty());
|
||||
QCOMPARE(spy.count(), 1);
|
||||
|
||||
LocalSend::SendSession session = mgr.sendSession(sessionId);
|
||||
QCOMPARE(session.sessionId, sessionId);
|
||||
QCOMPARE(session.target.alias, QStringLiteral("Receiver"));
|
||||
QCOMPARE(session.files.size(), 1);
|
||||
QCOMPARE(session.files[QStringLiteral("0")].localPath, QStringLiteral("/home/user/c.txt"));
|
||||
QCOMPARE(session.status, LocalSend::SessionStatus::Waiting);
|
||||
}
|
||||
|
||||
void TestSessionManager::testSetSendSessionTokens()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device target;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 100;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QMap<QString, QString> localPaths;
|
||||
localPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/a.txt"));
|
||||
|
||||
QString origSessionId = mgr.createSendSession(target, files, localPaths);
|
||||
|
||||
QString responseSessionId = QStringLiteral("server-session-123");
|
||||
QMap<QString, QString> tokens;
|
||||
tokens.insert(QStringLiteral("0"), QStringLiteral("token-abc"));
|
||||
|
||||
mgr.setSendSessionTokens(origSessionId, responseSessionId, tokens);
|
||||
|
||||
QVERIFY(!mgr.hasSendSession(origSessionId));
|
||||
QVERIFY(mgr.hasSendSession(responseSessionId));
|
||||
|
||||
LocalSend::SendSession session = mgr.sendSession(responseSessionId);
|
||||
QCOMPARE(session.sessionId, responseSessionId);
|
||||
QCOMPARE(session.files[QStringLiteral("0")].token, QStringLiteral("token-abc"));
|
||||
}
|
||||
|
||||
void TestSessionManager::testStartSendSession()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device target;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 100;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QMap<QString, QString> localPaths;
|
||||
localPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/a.txt"));
|
||||
|
||||
QString sessionId = mgr.createSendSession(target, files, localPaths);
|
||||
|
||||
QSignalSpy spy(&mgr, &LocalSend::SessionManager::sendSessionStarted);
|
||||
mgr.startSendSession(sessionId);
|
||||
|
||||
QCOMPARE(spy.count(), 1);
|
||||
LocalSend::SendSession session = mgr.sendSession(sessionId);
|
||||
QCOMPARE(session.status, LocalSend::SessionStatus::Sending);
|
||||
}
|
||||
|
||||
void TestSessionManager::testSendProgress()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device target;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 2000;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QMap<QString, QString> localPaths;
|
||||
localPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/a.txt"));
|
||||
|
||||
QString sessionId = mgr.createSendSession(target, files, localPaths);
|
||||
mgr.startSendSession(sessionId);
|
||||
|
||||
QSignalSpy spy(&mgr, &LocalSend::SessionManager::sendProgress);
|
||||
mgr.updateSendProgress(sessionId, QStringLiteral("0"), 1000);
|
||||
|
||||
QCOMPARE(spy.count(), 1);
|
||||
double progress = spy.takeFirst().at(2).toDouble();
|
||||
QVERIFY(qFuzzyCompare(progress, 0.5));
|
||||
}
|
||||
|
||||
void TestSessionManager::testCompleteSendFile()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device target;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 1000;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QMap<QString, QString> localPaths;
|
||||
localPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/a.txt"));
|
||||
|
||||
QString sessionId = mgr.createSendSession(target, files, localPaths);
|
||||
mgr.startSendSession(sessionId);
|
||||
|
||||
QSignalSpy spy(&mgr, &LocalSend::SessionManager::sendSessionCompleted);
|
||||
mgr.completeSendFile(sessionId, QStringLiteral("0"));
|
||||
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QVERIFY(!mgr.hasSendSession(sessionId));
|
||||
}
|
||||
|
||||
void TestSessionManager::testCompleteAllSendFiles()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device target;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 100;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
LocalSend::FileDto f2;
|
||||
f2.id = QStringLiteral("1");
|
||||
f2.fileName = QStringLiteral("b.txt");
|
||||
f2.size = 200;
|
||||
files.insert(QStringLiteral("1"), f2);
|
||||
|
||||
QMap<QString, QString> localPaths;
|
||||
localPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/a.txt"));
|
||||
localPaths.insert(QStringLiteral("1"), QStringLiteral("/tmp/b.txt"));
|
||||
|
||||
QString sessionId = mgr.createSendSession(target, files, localPaths);
|
||||
mgr.startSendSession(sessionId);
|
||||
|
||||
QSignalSpy spy(&mgr, &LocalSend::SessionManager::sendSessionCompleted);
|
||||
|
||||
mgr.completeSendFile(sessionId, QStringLiteral("0"));
|
||||
QCOMPARE(spy.count(), 0);
|
||||
|
||||
mgr.completeSendFile(sessionId, QStringLiteral("1"));
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QVERIFY(!mgr.hasSendSession(sessionId));
|
||||
}
|
||||
|
||||
void TestSessionManager::testCancelSendSession()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device target;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 100;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QMap<QString, QString> localPaths;
|
||||
localPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/a.txt"));
|
||||
|
||||
QString sessionId = mgr.createSendSession(target, files, localPaths);
|
||||
mgr.startSendSession(sessionId);
|
||||
|
||||
QSignalSpy spy(&mgr, &LocalSend::SessionManager::sendSessionCanceled);
|
||||
mgr.cancelSendSession(sessionId);
|
||||
|
||||
QCOMPARE(spy.count(), 1);
|
||||
QVERIFY(!mgr.hasSendSession(sessionId));
|
||||
}
|
||||
|
||||
void TestSessionManager::testHasSessionQueries()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
QVERIFY(!mgr.hasReceiveSession(QStringLiteral("nonexistent")));
|
||||
QVERIFY(!mgr.hasSendSession(QStringLiteral("nonexistent")));
|
||||
|
||||
LocalSend::Device sender;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 100;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QString recvSessionId = mgr.createReceiveSession(sender, files);
|
||||
QVERIFY(mgr.hasReceiveSession(recvSessionId));
|
||||
|
||||
QMap<QString, QString> localPaths;
|
||||
localPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/a.txt"));
|
||||
QString sendSessionId = mgr.createSendSession(sender, files, localPaths);
|
||||
QVERIFY(mgr.hasSendSession(sendSessionId));
|
||||
|
||||
LocalSend::ReceiveSession emptyRecv = mgr.receiveSession(QStringLiteral("nope"));
|
||||
QVERIFY(emptyRecv.sessionId.isEmpty());
|
||||
|
||||
LocalSend::SendSession emptySend = mgr.sendSession(QStringLiteral("nope"));
|
||||
QVERIFY(emptySend.sessionId.isEmpty());
|
||||
}
|
||||
|
||||
void TestSessionManager::testGenerateToken()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
QString token1 = mgr.generateToken();
|
||||
QString token2 = mgr.generateToken();
|
||||
|
||||
QVERIFY(!token1.isEmpty());
|
||||
QVERIFY(!token2.isEmpty());
|
||||
QVERIFY(token1 != token2);
|
||||
}
|
||||
|
||||
void TestSessionManager::testDeclineRemovesSession()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device sender;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 100;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
QString sessionId = mgr.createReceiveSession(sender, files);
|
||||
QVERIFY(mgr.hasReceiveSession(sessionId));
|
||||
|
||||
mgr.declineReceiveSession(sessionId);
|
||||
QVERIFY(!mgr.hasReceiveSession(sessionId));
|
||||
}
|
||||
|
||||
void TestSessionManager::testSetSendSessionTokensRekeys()
|
||||
{
|
||||
LocalSend::SessionManager mgr;
|
||||
|
||||
LocalSend::Device target;
|
||||
QMap<QString, LocalSend::FileDto> files;
|
||||
LocalSend::FileDto f1;
|
||||
f1.id = QStringLiteral("0");
|
||||
f1.fileName = QStringLiteral("a.txt");
|
||||
f1.size = 100;
|
||||
files.insert(QStringLiteral("0"), f1);
|
||||
|
||||
LocalSend::FileDto f2;
|
||||
f2.id = QStringLiteral("1");
|
||||
f2.fileName = QStringLiteral("b.txt");
|
||||
f2.size = 200;
|
||||
files.insert(QStringLiteral("1"), f2);
|
||||
|
||||
QMap<QString, QString> localPaths;
|
||||
localPaths.insert(QStringLiteral("0"), QStringLiteral("/tmp/a.txt"));
|
||||
localPaths.insert(QStringLiteral("1"), QStringLiteral("/tmp/b.txt"));
|
||||
|
||||
QString origId = mgr.createSendSession(target, files, localPaths);
|
||||
|
||||
QString newId = QStringLiteral("response-session-id");
|
||||
QMap<QString, QString> tokens;
|
||||
tokens.insert(QStringLiteral("0"), QStringLiteral("tok0"));
|
||||
tokens.insert(QStringLiteral("1"), QStringLiteral("tok1"));
|
||||
|
||||
mgr.setSendSessionTokens(origId, newId, tokens);
|
||||
|
||||
QVERIFY(!mgr.hasSendSession(origId));
|
||||
QVERIFY(mgr.hasSendSession(newId));
|
||||
|
||||
LocalSend::SendSession session = mgr.sendSession(newId);
|
||||
QCOMPARE(session.sessionId, newId);
|
||||
QCOMPARE(session.files[QStringLiteral("0")].token, QStringLiteral("tok0"));
|
||||
QCOMPARE(session.files[QStringLiteral("1")].token, QStringLiteral("tok1"));
|
||||
QCOMPARE(session.files[QStringLiteral("0")].localPath, QStringLiteral("/tmp/a.txt"));
|
||||
QCOMPARE(session.files[QStringLiteral("1")].localPath, QStringLiteral("/tmp/b.txt"));
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestSessionManager)
|
||||
#include "TestSessionManager.moc"
|
||||
82
tests/core/TestTypes.cpp
Normal file
82
tests/core/TestTypes.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include <QtTest>
|
||||
#include <LocalSendCore/Types.h>
|
||||
|
||||
class TestTypes : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void testDeviceTypeToString();
|
||||
void testDeviceTypeFromString();
|
||||
void testDeviceTypeRoundTrip();
|
||||
void testDeviceTypeFromStringUnknown();
|
||||
void testProtocolTypeToString();
|
||||
void testProtocolTypeFromString();
|
||||
void testProtocolTypeRoundTrip();
|
||||
void testProtocolTypeFromStringUnknown();
|
||||
};
|
||||
|
||||
void TestTypes::testDeviceTypeToString()
|
||||
{
|
||||
QCOMPARE(LocalSend::deviceTypeToString(LocalSend::DeviceType::Mobile), QStringLiteral("mobile"));
|
||||
QCOMPARE(LocalSend::deviceTypeToString(LocalSend::DeviceType::Desktop), QStringLiteral("desktop"));
|
||||
QCOMPARE(LocalSend::deviceTypeToString(LocalSend::DeviceType::Web), QStringLiteral("web"));
|
||||
QCOMPARE(LocalSend::deviceTypeToString(LocalSend::DeviceType::Headless), QStringLiteral("headless"));
|
||||
QCOMPARE(LocalSend::deviceTypeToString(LocalSend::DeviceType::Server), QStringLiteral("server"));
|
||||
}
|
||||
|
||||
void TestTypes::testDeviceTypeFromString()
|
||||
{
|
||||
QCOMPARE(LocalSend::deviceTypeFromString(QStringLiteral("mobile")), LocalSend::DeviceType::Mobile);
|
||||
QCOMPARE(LocalSend::deviceTypeFromString(QStringLiteral("desktop")), LocalSend::DeviceType::Desktop);
|
||||
QCOMPARE(LocalSend::deviceTypeFromString(QStringLiteral("web")), LocalSend::DeviceType::Web);
|
||||
QCOMPARE(LocalSend::deviceTypeFromString(QStringLiteral("headless")), LocalSend::DeviceType::Headless);
|
||||
QCOMPARE(LocalSend::deviceTypeFromString(QStringLiteral("server")), LocalSend::DeviceType::Server);
|
||||
}
|
||||
|
||||
void TestTypes::testDeviceTypeRoundTrip()
|
||||
{
|
||||
const QList<LocalSend::DeviceType> types = {
|
||||
LocalSend::DeviceType::Mobile,
|
||||
LocalSend::DeviceType::Desktop,
|
||||
LocalSend::DeviceType::Web,
|
||||
LocalSend::DeviceType::Headless,
|
||||
LocalSend::DeviceType::Server,
|
||||
};
|
||||
for (const auto& type : types) {
|
||||
QCOMPARE(LocalSend::deviceTypeFromString(LocalSend::deviceTypeToString(type)), type);
|
||||
}
|
||||
}
|
||||
|
||||
void TestTypes::testDeviceTypeFromStringUnknown()
|
||||
{
|
||||
QCOMPARE(LocalSend::deviceTypeFromString(QStringLiteral("unknown")), LocalSend::DeviceType::Desktop);
|
||||
QCOMPARE(LocalSend::deviceTypeFromString(QString()), LocalSend::DeviceType::Desktop);
|
||||
}
|
||||
|
||||
void TestTypes::testProtocolTypeToString()
|
||||
{
|
||||
QCOMPARE(LocalSend::protocolTypeToString(LocalSend::ProtocolType::Http), QStringLiteral("http"));
|
||||
QCOMPARE(LocalSend::protocolTypeToString(LocalSend::ProtocolType::Https), QStringLiteral("https"));
|
||||
}
|
||||
|
||||
void TestTypes::testProtocolTypeFromString()
|
||||
{
|
||||
QCOMPARE(LocalSend::protocolTypeFromString(QStringLiteral("http")), LocalSend::ProtocolType::Http);
|
||||
QCOMPARE(LocalSend::protocolTypeFromString(QStringLiteral("https")), LocalSend::ProtocolType::Https);
|
||||
}
|
||||
|
||||
void TestTypes::testProtocolTypeRoundTrip()
|
||||
{
|
||||
QCOMPARE(LocalSend::protocolTypeFromString(LocalSend::protocolTypeToString(LocalSend::ProtocolType::Http)), LocalSend::ProtocolType::Http);
|
||||
QCOMPARE(LocalSend::protocolTypeFromString(LocalSend::protocolTypeToString(LocalSend::ProtocolType::Https)), LocalSend::ProtocolType::Https);
|
||||
}
|
||||
|
||||
void TestTypes::testProtocolTypeFromStringUnknown()
|
||||
{
|
||||
QCOMPARE(LocalSend::protocolTypeFromString(QStringLiteral("ftp")), LocalSend::ProtocolType::Http);
|
||||
QCOMPARE(LocalSend::protocolTypeFromString(QString()), LocalSend::ProtocolType::Http);
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestTypes)
|
||||
#include "TestTypes.moc"
|
||||
Reference in New Issue
Block a user