diff --git a/cli/kdeconnect-cli.cpp b/cli/kdeconnect-cli.cpp index 2b5e4de2..f44001d2 100644 --- a/cli/kdeconnect-cli.cpp +++ b/cli/kdeconnect-cli.cpp @@ -1,268 +1,272 @@ /* * Copyright 2015 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include "interfaces/devicesmodel.h" #include "interfaces/notificationsmodel.h" #include "interfaces/dbusinterfaces.h" #include "interfaces/dbushelpers.h" #include "kdeconnect-version.h" int main(int argc, char** argv) { QCoreApplication app(argc, argv); KAboutData about(QStringLiteral("kdeconnect-cli"), QStringLiteral("kdeconnect-cli"), QStringLiteral(KDECONNECT_VERSION_STRING), i18n("KDE Connect CLI tool"), KAboutLicense::GPL, i18n("(C) 2015 Aleix Pol Gonzalez")); KAboutData::setApplicationData(about); about.addAuthor( i18n("Aleix Pol Gonzalez"), QString(), QStringLiteral("aleixpol@kde.org") ); about.addAuthor( i18n("Albert Vaca Cintora"), QString(), QStringLiteral("albertvaka@gmail.com") ); QCommandLineParser parser; parser.addOption(QCommandLineOption(QStringList(QStringLiteral("l")) << QStringLiteral("list-devices"), i18n("List all devices"))); parser.addOption(QCommandLineOption(QStringList(QStringLiteral("a")) << QStringLiteral("list-available"), i18n("List available (paired and reachable) devices"))); parser.addOption(QCommandLineOption(QStringLiteral("id-only"), i18n("Make --list-devices or --list-available print only the devices id, to ease scripting"))); parser.addOption(QCommandLineOption(QStringLiteral("name-only"), i18n("Make --list-devices or --list-available print only the devices name, to ease scripting"))); parser.addOption(QCommandLineOption(QStringLiteral("id-name-only"), i18n("Make --list-devices or --list-available print only the devices id and name, to ease scripting"))); parser.addOption(QCommandLineOption(QStringLiteral("refresh"), i18n("Search for devices in the network and re-establish connections"))); parser.addOption(QCommandLineOption(QStringLiteral("pair"), i18n("Request pairing to a said device"))); parser.addOption(QCommandLineOption(QStringLiteral("ring"), i18n("Find the said device by ringing it."))); parser.addOption(QCommandLineOption(QStringLiteral("unpair"), i18n("Stop pairing to a said device"))); parser.addOption(QCommandLineOption(QStringLiteral("ping"), i18n("Sends a ping to said device"))); parser.addOption(QCommandLineOption(QStringLiteral("ping-msg"), i18n("Same as ping but you can set the message to display"), i18n("message"))); parser.addOption(QCommandLineOption(QStringLiteral("share"), i18n("Share a file to a said device"), QStringLiteral("path"))); parser.addOption(QCommandLineOption(QStringLiteral("list-notifications"), i18n("Display the notifications on a said device"))); parser.addOption(QCommandLineOption(QStringLiteral("lock"), i18n("Lock the specified device"))); parser.addOption(QCommandLineOption(QStringLiteral("send-sms"), i18n("Sends an SMS. Requires destination"), i18n("message"))); parser.addOption(QCommandLineOption(QStringLiteral("destination"), i18n("Phone number to send the message"), i18n("phone number"))); parser.addOption(QCommandLineOption(QStringList(QStringLiteral("device")) << QStringLiteral("d"), i18n("Device ID"), QStringLiteral("dev"))); parser.addOption(QCommandLineOption(QStringList(QStringLiteral("name")) << QStringLiteral("n"), i18n("Device Name"), QStringLiteral("name"))); parser.addOption(QCommandLineOption(QStringLiteral("encryption-info"), i18n("Get encryption info about said device"))); parser.addOption(QCommandLineOption(QStringLiteral("list-commands"), i18n("Lists remote commands and their ids"))); parser.addOption(QCommandLineOption(QStringLiteral("execute-command"), i18n("Executes a remote command by id"), QStringLiteral("id"))); parser.addOption(QCommandLineOption(QStringList{QStringLiteral("k"), QStringLiteral("send-keys")}, i18n("Sends keys to a said device"))); parser.addOption(QCommandLineOption(QStringLiteral("my-id"), i18n("Display this device's id and exit"))); about.setupCommandLine(&parser); parser.addHelpOption(); parser.process(app); about.processCommandLine(&parser); const QString id = "kdeconnect-cli-"+QString::number(QCoreApplication::applicationPid()); DaemonDbusInterface iface; if (parser.isSet(QStringLiteral("my-id"))) { QTextStream(stdout) << iface.selfId() << endl; } else if (parser.isSet(QStringLiteral("l")) || parser.isSet(QStringLiteral("a"))) { bool paired = true, reachable = false; if (parser.isSet(QStringLiteral("a"))) { reachable = true; } else { blockOnReply(iface.acquireDiscoveryMode(id)); QThread::sleep(2); } const QStringList devices = blockOnReply(iface.devices(reachable, paired)); bool displayCount = true; for (const QString& id : devices) { if (parser.isSet(QStringLiteral("id-only"))) { QTextStream(stdout) << id << endl; displayCount = false; } else if (parser.isSet(QStringLiteral("name-only"))) { DeviceDbusInterface deviceIface(id); QTextStream(stdout) << deviceIface.name() << endl; displayCount = false; } else if (parser.isSet(QStringLiteral("id-name-only"))) { DeviceDbusInterface deviceIface(id); QTextStream(stdout) << id << ' ' << deviceIface.name() << endl; displayCount = false; } else { DeviceDbusInterface deviceIface(id); QString statusInfo; const bool isReachable = deviceIface.isReachable(); const bool isTrusted = deviceIface.isTrusted(); if (isReachable && isTrusted) { statusInfo = i18n("(paired and reachable)"); } else if (isReachable) { statusInfo = i18n("(reachable)"); } else if (isTrusted) { statusInfo = i18n("(paired)"); } QTextStream(stdout) << "- " << deviceIface.name() << ": " << deviceIface.id() << ' ' << statusInfo << endl; } } if (displayCount) { QTextStream(stdout) << i18np("1 device found", "%1 devices found", devices.size()) << endl; } else if (devices.isEmpty()) { QTextStream(stderr) << i18n("No devices found") << endl; } blockOnReply(iface.releaseDiscoveryMode(id)); } else if(parser.isSet(QStringLiteral("refresh"))) { QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), QStringLiteral("/modules/kdeconnect"), QStringLiteral("org.kde.kdeconnect.daemon"), QStringLiteral("forceOnNetworkChange")); blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); } else { QString device = parser.value(QStringLiteral("device")); if (device.isEmpty() && parser.isSet(QStringLiteral("name"))) { device = blockOnReply(iface.deviceIdByName(parser.value(QStringLiteral("name")))); if (device.isEmpty()) { QTextStream(stderr) << "Couldn't find device: " << parser.value(QStringLiteral("name")) << endl; return 1; } } if(device.isEmpty()) { QTextStream(stderr) << i18n("No device specified") << endl; parser.showHelp(1); } if (parser.isSet(QStringLiteral("share"))) { - QList urls; + QStringList urls; + QUrl url = QUrl::fromUserInput(parser.value(QStringLiteral("share")), QDir::currentPath()); - urls.append(url); + urls.append(url.toString()); // Check for more arguments const auto args = parser.positionalArguments(); for (const QString& input : args) { QUrl url = QUrl::fromUserInput(input, QDir::currentPath()); - urls.append(url); + urls.append(url.toString()); } - for (const QUrl& url : urls) { - QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), "/modules/kdeconnect/devices/"+device+"/share", QStringLiteral("org.kde.kdeconnect.device.share"), QStringLiteral("shareUrl")); - msg.setArguments(QVariantList() << url.toString()); - blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); - QTextStream(stdout) << i18n("Shared %1", url.toString()) << endl; + QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), "/modules/kdeconnect/devices/"+device+"/share", + QStringLiteral("org.kde.kdeconnect.device.share"), QStringLiteral("shareUrls")); + + msg.setArguments(QVariantList() << QVariant(urls)); + blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); + + for (const QString& url : qAsConst(urls)) { + QTextStream(stdout) << i18n("Shared %1", url) << endl; } } else if(parser.isSet(QStringLiteral("pair"))) { DeviceDbusInterface dev(device); if (!dev.isReachable()) { //Device doesn't exist, go into discovery mode and wait up to 30 seconds for the device to appear QEventLoop wait; QTextStream(stderr) << i18n("waiting for device...") << endl; blockOnReply(iface.acquireDiscoveryMode(id)); QObject::connect(&iface, &DaemonDbusInterface::deviceAdded, [&](const QString& deviceAddedId) { if (device == deviceAddedId) { wait.quit(); } }); QTimer::singleShot(30 * 1000, &wait, &QEventLoop::quit); wait.exec(); } if (!dev.isReachable()) { QTextStream(stderr) << i18n("Device not found") << endl; } else if(blockOnReply(dev.isTrusted())) { QTextStream(stderr) << i18n("Already paired") << endl; } else { QTextStream(stderr) << i18n("Pair requested") << endl; blockOnReply(dev.requestPair()); } blockOnReply(iface.releaseDiscoveryMode(id)); } else if(parser.isSet(QStringLiteral("unpair"))) { DeviceDbusInterface dev(device); if (!dev.isTrusted()) { QTextStream(stderr) << i18n("Already not paired") << endl; } else { QTextStream(stderr) << i18n("Unpaired") << endl; blockOnReply(dev.unpair()); } } else if(parser.isSet(QStringLiteral("ping")) || parser.isSet(QStringLiteral("ping-msg"))) { QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), "/modules/kdeconnect/devices/"+device+"/ping", QStringLiteral("org.kde.kdeconnect.device.ping"), QStringLiteral("sendPing")); if (parser.isSet(QStringLiteral("ping-msg"))) { QString message = parser.value(QStringLiteral("ping-msg")); msg.setArguments(QVariantList() << message); } blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); } else if(parser.isSet(QStringLiteral("send-sms"))) { if (parser.isSet(QStringLiteral("destination"))) { QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), "/modules/kdeconnect/devices/"+device+"/sms", QStringLiteral("org.kde.kdeconnect.device.sms"), QStringLiteral("sendSms")); msg.setArguments({ parser.value("destination"), parser.value("send-sms") }); blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); } else { QTextStream(stderr) << i18n("error: should specify the SMS's recipient by passing --destination "); return 1; } } else if(parser.isSet(QStringLiteral("ring"))) { QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), "/modules/kdeconnect/devices/"+device+"/findmyphone", QStringLiteral("org.kde.kdeconnect.device.findmyphone"), QStringLiteral("ring")); blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); } else if(parser.isSet("send-keys")) { QString seq = parser.value("send-keys"); QDBusMessage msg = QDBusMessage::createMethodCall("org.kde.kdeconnect", "/modules/kdeconnect/devices/"+device+"/remotekeyboard", "org.kde.kdeconnect.device.remotekeyboard", "sendKeyPress"); if (seq.trimmed() == QLatin1String("-")) { // from stdin QFile in; if(in.open(stdin,QIODevice::ReadOnly | QIODevice::Unbuffered)) { while (!in.atEnd()) { QByteArray line = in.readLine(); // sanitize to ASCII-codes > 31? msg.setArguments({QString(line), -1, false, false, false}); blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); } in.close(); } } else { msg.setArguments({seq, -1, false, false, false}); blockOnReply(QDBusConnection::sessionBus().asyncCall(msg)); } } else if(parser.isSet(QStringLiteral("list-notifications"))) { NotificationsModel notifications; notifications.setDeviceId(device); for(int i=0, rows=notifications.rowCount(); itoObject(); QTextStream(stdout) << it.key() << ": " << cont.value(QStringLiteral("name")).toString() << ": " << cont.value(QStringLiteral("command")).toString() << endl; } } else if(parser.isSet(QStringLiteral("execute-command"))) { RemoteCommandsDbusInterface iface(device); blockOnReply(iface.triggerCommand(parser.value(QStringLiteral("execute-command")))); } else if(parser.isSet(QStringLiteral("encryption-info"))) { DeviceDbusInterface dev(device); QString info = blockOnReply(dev.encryptionInfo()); // QSsl::Der = 1 QTextStream(stdout) << info << endl; } else { QTextStream(stderr) << i18n("Nothing to be done") << endl; } } QMetaObject::invokeMethod(&app, "quit", Qt::QueuedConnection); return app.exec(); } diff --git a/core/backends/lan/CMakeLists.txt b/core/backends/lan/CMakeLists.txt index 1637f1a3..6a838efb 100644 --- a/core/backends/lan/CMakeLists.txt +++ b/core/backends/lan/CMakeLists.txt @@ -1,13 +1,14 @@ set(backends_kdeconnect_SRCS ${backends_kdeconnect_SRCS} backends/lan/server.cpp backends/lan/lanlinkprovider.cpp backends/lan/landevicelink.cpp backends/lan/lanpairinghandler.cpp + backends/lan/compositeuploadjob.cpp backends/lan/uploadjob.cpp backends/lan/socketlinereader.cpp PARENT_SCOPE ) diff --git a/core/backends/lan/compositeuploadjob.cpp b/core/backends/lan/compositeuploadjob.cpp new file mode 100644 index 00000000..f74baebb --- /dev/null +++ b/core/backends/lan/compositeuploadjob.cpp @@ -0,0 +1,275 @@ +/** + * Copyright 2018 Erik Duisters + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "compositeuploadjob.h" +#include +#include +#include +#include +#include "lanlinkprovider.h" +#include + +CompositeUploadJob::CompositeUploadJob(const QString& deviceId, bool displayNotification) + : KCompositeJob() + , m_server(new Server(this)) + , m_socket(nullptr) + , m_port(0) + , m_deviceId(deviceId) + , m_running(false) + , m_currentJobNum(1) + , m_totalJobs(0) + , m_currentJobSendPayloadSize(0) + , m_totalSendPayloadSize(0) + , m_totalPayloadSize(0) + , m_currentJob(nullptr) +{ + setCapabilities(Killable); + + if (displayNotification) { + KIO::getJobTracker()->registerJob(this); + } +} + +bool CompositeUploadJob::isRunning() +{ + return m_running; +} + +void CompositeUploadJob::start() { + if (m_running) { + qCWarning(KDECONNECT_CORE) << "CompositeUploadJob::start() - allready running"; + return; + } + + if (!hasSubjobs()) { + qCWarning(KDECONNECT_CORE) << "CompositeUploadJob::start() - there are no subjobs to start"; + emitResult(); + return; + } + + if (!startListening()) { + return; + } + + connect(m_server, &QTcpServer::newConnection, this, &CompositeUploadJob::newConnection); + + m_running = true; + + //Give SharePlugin some time to add subjobs + QMetaObject::invokeMethod(this, "startNextSubJob", Qt::QueuedConnection); +} + +bool CompositeUploadJob::startListening() +{ + m_port = MIN_PORT; + while (!m_server->listen(QHostAddress::Any, m_port)) { + m_port++; + if (m_port > MAX_PORT) { //No ports available? + qCWarning(KDECONNECT_CORE) << "CompositeUploadJob::startListening() - Error opening a port in range" << MIN_PORT << "-" << MAX_PORT; + m_port = 0; + setError(NoPortAvailable); + setErrorText(i18n("Couldn't find an available port")); + emitResult(); + return false; + } + } + + qCDebug(KDECONNECT_CORE) << "CompositeUploadJob::startListening() - listening on port: " << m_port; + return true; +} + +void CompositeUploadJob::startNextSubJob() +{ + m_currentJob = qobject_cast(subjobs().at(0)); + m_currentJobSendPayloadSize = 0; + emitDescription(m_currentJob->getNetworkPacket().get(QStringLiteral("filename"))); + + connect(m_currentJob, SIGNAL(processedAmount(KJob*,KJob::Unit,qulonglong)), this, SLOT(slotProcessedAmount(KJob*,KJob::Unit,qulonglong))); + //Already done by KCompositeJob + //connect(m_currentJob, &KJob::result, this, &CompositeUploadJob::slotResult); + + //TODO: Create a copy of the networkpacket that can be re-injected if sending via lan fails? + NetworkPacket np = m_currentJob->getNetworkPacket(); + np.setPayload(nullptr, np.payloadSize()); + np.setPayloadTransferInfo({{"port", m_port}}); + np.set(QStringLiteral("numberOfFiles"), m_totalJobs); + np.set(QStringLiteral("totalPayloadSize"), m_totalPayloadSize); + + if (Daemon::instance()->getDevice(m_deviceId)->sendPacket(np)) { + m_server->resumeAccepting(); + } else { + setError(SendingNetworkPacketFailed); + setErrorText(i18n("Failed to send packet to %1", Daemon::instance()->getDevice(m_deviceId)->name())); + //TODO: cleanup/resend remaining jobs + emitResult(); + } +} + +void CompositeUploadJob::newConnection() +{ + m_server->pauseAccepting(); + + m_socket = m_server->nextPendingConnection(); + + if (!m_socket) { + qCDebug(KDECONNECT_CORE) << "CompositeUploadJob::newConnection() - m_server->nextPendingConnection() returned a nullptr"; + return; + } + + m_currentJob->setSocket(m_socket); + + connect(m_socket, &QSslSocket::disconnected, this, &CompositeUploadJob::socketDisconnected); + connect(m_socket, QOverload::of(&QAbstractSocket::error), this, &CompositeUploadJob::socketError); + connect(m_socket, QOverload &>::of(&QSslSocket::sslErrors), this, &CompositeUploadJob::sslError); + connect(m_socket, &QSslSocket::encrypted, this, &CompositeUploadJob::encrypted); + + LanLinkProvider::configureSslSocket(m_socket, m_deviceId, true); + + m_socket->startServerEncryption(); +} + +void CompositeUploadJob::socketDisconnected() +{ + m_socket->close(); +} + +void CompositeUploadJob::socketError(QAbstractSocket::SocketError error) +{ + Q_UNUSED(error); + + m_socket->close(); + setError(SocketError); + emitResult(); + //TODO: cleanup jobs? + m_running = false; +} + +void CompositeUploadJob::sslError(const QList& errors) +{ + Q_UNUSED(errors); + + m_socket->close(); + setError(SslError); + emitResult(); + //TODO: cleanup jobs? + m_running = false; +} + +void CompositeUploadJob::encrypted() +{ + if (!m_timer.isValid()) { + m_timer.start(); + } + + m_currentJob->start(); +} + +bool CompositeUploadJob::addSubjob(KJob* job) +{ + if (UploadJob *uploadJob = qobject_cast(job)) { + NetworkPacket np = uploadJob->getNetworkPacket(); + + m_totalJobs++; + + if (np.payloadSize() >= 0 ) { + m_totalPayloadSize += np.payloadSize(); + setTotalAmount(Bytes, m_totalPayloadSize); + } + + QString filename; + QString filenameArg = QStringLiteral("filename"); + + if (m_currentJob) { + filename = m_currentJob->getNetworkPacket().get(filenameArg); + } else { + filename = np.get(filenameArg); + } + + emitDescription(filename); + + return KCompositeJob::addSubjob(job); + } else { + qCDebug(KDECONNECT_CORE) << "CompositeUploadJob::addSubjob() - you can only add UploadJob's, ignoring"; + return false; + } +} + +bool CompositeUploadJob::doKill() +{ + //TODO: Remove all subjobs? + //TODO: cleanup jobs? + if (m_running) { + m_running = false; + + return m_currentJob->stop(); + } + + return true; +} + +void CompositeUploadJob::slotProcessedAmount(KJob *job, KJob::Unit unit, qulonglong amount) { + Q_UNUSED(job); + + m_currentJobSendPayloadSize = amount; + + quint64 uploaded = m_totalSendPayloadSize + m_currentJobSendPayloadSize; + setProcessedAmount(unit, uploaded); + + const auto elapsed = m_timer.elapsed(); + if (elapsed > 0) { + emitSpeed((1000 * uploaded) / elapsed); + } +} + +void CompositeUploadJob::slotResult(KJob *job) { + //Copies job error and errorText and emits result if job is in error otherwise removes job from subjob list + KCompositeJob::slotResult(job); + + //TODO: cleanup jobs? + + if (error() || !m_running) { + return; + } + + m_totalSendPayloadSize += m_currentJobSendPayloadSize; + + if (hasSubjobs()) { + m_currentJobNum++; + startNextSubJob(); + } else { + Q_EMIT description(this, i18n("Finished sending to %1", Daemon::instance()->getDevice(this->m_deviceId)->name()), + { QStringLiteral(""), i18np("Sent 1 file", "Sent %1 files", m_totalJobs) } + ); + emitResult(); + } +} + +void CompositeUploadJob::emitDescription(const QString& currentFileName) { + QPair field2; + + if (m_totalJobs > 1) { + field2.first = i18n("Progress"); + field2.second = i18n("Sending file %1 of %2", m_currentJobNum, m_totalJobs); + } + + Q_EMIT description(this, i18n("Sending to %1", Daemon::instance()->getDevice(this->m_deviceId)->name()), + { i18n("File"), currentFileName }, field2 + ); +} diff --git a/core/backends/lan/compositeuploadjob.h b/core/backends/lan/compositeuploadjob.h new file mode 100644 index 00000000..d4d65d87 --- /dev/null +++ b/core/backends/lan/compositeuploadjob.h @@ -0,0 +1,84 @@ +/** + * Copyright 2018 Erik Duisters + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef COMPOSITEUPLOADJOB_H +#define COMPOSITEUPLOADJOB_H + +#include "kdeconnectcore_export.h" +#include +#include "server.h" +#include "uploadjob.h" + +class KDECONNECTCORE_EXPORT CompositeUploadJob + : public KCompositeJob +{ + Q_OBJECT + +public: + explicit CompositeUploadJob(const QString& deviceId, bool displayNotification); + + void start() override; + QVariantMap transferInfo(); + bool isRunning(); + bool addSubjob(KJob* job) override; + +private: + bool startListening(); + void emitDescription(const QString& currentFileName); + +protected: + bool doKill() override; + +private: + enum { + NoPortAvailable = UserDefinedError, + SendingNetworkPacketFailed, + SocketError, + SslError + }; + + Server *const m_server; + QSslSocket *m_socket; + quint16 m_port; + const QString& m_deviceId; + bool m_running; + int m_currentJobNum; + int m_totalJobs; + quint64 m_currentJobSendPayloadSize; + quint64 m_totalSendPayloadSize; + quint64 m_totalPayloadSize; + UploadJob *m_currentJob; + QElapsedTimer m_timer; + + const static quint16 MIN_PORT = 1739; + const static quint16 MAX_PORT = 1764; + +private Q_SLOTS: + void newConnection(); + void socketDisconnected(); + void socketError(QAbstractSocket::SocketError socketError); + void sslError(const QList& errors); + void encrypted(); + void slotProcessedAmount(KJob *job, KJob::Unit unit, qulonglong amount); + void slotResult(KJob *job) override; + void startNextSubJob(); +}; + +#endif //COMPOSITEUPLOADJOB_H diff --git a/core/backends/lan/landevicelink.cpp b/core/backends/lan/landevicelink.cpp index 9fb8f4ba..e88e1e3c 100644 --- a/core/backends/lan/landevicelink.cpp +++ b/core/backends/lan/landevicelink.cpp @@ -1,189 +1,193 @@ /** * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include "landevicelink.h" #include "core_debug.h" #include "kdeconnectconfig.h" #include "backends/linkprovider.h" #include "socketlinereader.h" #include "lanlinkprovider.h" -#include -#include -#include +#include "plugins/share/shareplugin.h" LanDeviceLink::LanDeviceLink(const QString& deviceId, LinkProvider* parent, QSslSocket* socket, ConnectionStarted connectionSource) : DeviceLink(deviceId, parent) , m_socketLineReader(nullptr) { reset(socket, connectionSource); } void LanDeviceLink::reset(QSslSocket* socket, ConnectionStarted connectionSource) { if (m_socketLineReader) { disconnect(m_socketLineReader->m_socket, &QAbstractSocket::disconnected, this, &QObject::deleteLater); delete m_socketLineReader; } m_socketLineReader = new SocketLineReader(socket, this); connect(socket, &QAbstractSocket::disconnected, this, &QObject::deleteLater); connect(m_socketLineReader, &SocketLineReader::readyRead, this, &LanDeviceLink::dataReceived); //We take ownership of the socket. //When the link provider destroys us, //the socket (and the reader) will be //destroyed as well socket->setParent(m_socketLineReader); m_connectionSource = connectionSource; QString certString = KdeConnectConfig::instance()->getDeviceProperty(deviceId(), QStringLiteral("certificate")); DeviceLink::setPairStatus(certString.isEmpty()? PairStatus::NotPaired : PairStatus::Paired); } QHostAddress LanDeviceLink::hostAddress() const { if (!m_socketLineReader) { return QHostAddress::Null; } QHostAddress addr = m_socketLineReader->m_socket->peerAddress(); if (addr.protocol() == QAbstractSocket::IPv6Protocol) { bool success; QHostAddress convertedAddr = QHostAddress(addr.toIPv4Address(&success)); if (success) { qCDebug(KDECONNECT_CORE) << "Converting IPv6" << addr << "to IPv4" << convertedAddr; addr = convertedAddr; } } return addr; } QString LanDeviceLink::name() { return QStringLiteral("LanLink"); // Should be same in both android and kde version } bool LanDeviceLink::sendPacket(NetworkPacket& np) { - if (np.hasPayload()) { - np.setPayloadTransferInfo(sendPayload(np)->transferInfo()); - } - - int written = m_socketLineReader->write(np.serialize()); - - //Actually we can't detect if a packet is received or not. We keep TCP - //"ESTABLISHED" connections that look legit (return true when we use them), - //but that are actually broken (until keepalive detects that they are down). - return (written != -1); -} + if (np.payload()) { + if (np.type() == PACKET_TYPE_SHARE_REQUEST && np.payloadSize() >= 0) { + if (!m_compositeUploadJob || !m_compositeUploadJob->isRunning()) { + m_compositeUploadJob = new CompositeUploadJob(deviceId(), true); + } + + m_compositeUploadJob->addSubjob(new UploadJob(np)); + + if (!m_compositeUploadJob->isRunning()) { + m_compositeUploadJob->start(); + } + } else { //Infinite stream + CompositeUploadJob* fireAndForgetJob = new CompositeUploadJob(deviceId(), false); + fireAndForgetJob->addSubjob(new UploadJob(np)); + fireAndForgetJob->start(); + } + + return true; + } else { + int written = m_socketLineReader->write(np.serialize()); -UploadJob* LanDeviceLink::sendPayload(const NetworkPacket& np) -{ - UploadJob* job = new UploadJob(np.payload(), deviceId()); - if (np.type() == PACKET_TYPE_SHARE_REQUEST && np.payloadSize() >= 0) { - KIO::getJobTracker()->registerJob(job); + //Actually we can't detect if a packet is received or not. We keep TCP + //"ESTABLISHED" connections that look legit (return true when we use them), + //but that are actually broken (until keepalive detects that they are down). + return (written != -1); } - job->start(); - return job; } void LanDeviceLink::dataReceived() { if (m_socketLineReader->bytesAvailable() == 0) return; const QByteArray serializedPacket = m_socketLineReader->readLine(); NetworkPacket packet(QString::null); NetworkPacket::unserialize(serializedPacket, &packet); //qCDebug(KDECONNECT_CORE) << "LanDeviceLink dataReceived" << serializedPacket; if (packet.type() == PACKET_TYPE_PAIR) { //TODO: Handle pair/unpair requests and forward them (to the pairing handler?) qobject_cast(provider())->incomingPairPacket(this, packet); return; } if (packet.hasPayloadTransferInfo()) { //qCDebug(KDECONNECT_CORE) << "HasPayloadTransferInfo"; const QVariantMap transferInfo = packet.payloadTransferInfo(); QSharedPointer socket(new QSslSocket); LanLinkProvider::configureSslSocket(socket.data(), deviceId(), true); // emit readChannelFinished when the socket gets disconnected. This seems to be a bug in upstream QSslSocket. // Needs investigation and upstreaming of the fix. QTBUG-62257 connect(socket.data(), &QAbstractSocket::disconnected, socket.data(), &QAbstractSocket::readChannelFinished); const QString address = m_socketLineReader->peerAddress().toString(); const quint16 port = transferInfo[QStringLiteral("port")].toInt(); socket->connectToHostEncrypted(address, port, QIODevice::ReadWrite); packet.setPayload(socket, packet.payloadSize()); } Q_EMIT receivedPacket(packet); if (m_socketLineReader->bytesAvailable() > 0) { QMetaObject::invokeMethod(this, "dataReceived", Qt::QueuedConnection); } } void LanDeviceLink::userRequestsPair() { if (m_socketLineReader->peerCertificate().isNull()) { Q_EMIT pairingError(i18n("This device cannot be paired because it is running an old version of KDE Connect.")); } else { qobject_cast(provider())->userRequestsPair(deviceId()); } } void LanDeviceLink::userRequestsUnpair() { qobject_cast(provider())->userRequestsUnpair(deviceId()); } void LanDeviceLink::setPairStatus(PairStatus status) { if (status == Paired && m_socketLineReader->peerCertificate().isNull()) { Q_EMIT pairingError(i18n("This device cannot be paired because it is running an old version of KDE Connect.")); return; } DeviceLink::setPairStatus(status); if (status == Paired) { Q_ASSERT(KdeConnectConfig::instance()->trustedDevices().contains(deviceId())); Q_ASSERT(!m_socketLineReader->peerCertificate().isNull()); KdeConnectConfig::instance()->setDeviceProperty(deviceId(), QStringLiteral("certificate"), m_socketLineReader->peerCertificate().toPem()); } } bool LanDeviceLink::linkShouldBeKeptAlive() { return true; //FIXME: Current implementation is broken, so for now we will keep links always established //We keep the remotely initiated connections, since the remotes require them if they want to request //pairing to us, or connections that are already paired. TODO: Keep connections in the process of pairing //return (mConnectionSource == ConnectionStarted::Remotely || pairStatus() == Paired); } diff --git a/core/backends/lan/landevicelink.h b/core/backends/lan/landevicelink.h index 3928ff37..2a62fb63 100644 --- a/core/backends/lan/landevicelink.h +++ b/core/backends/lan/landevicelink.h @@ -1,68 +1,70 @@ /** * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef LANDEVICELINK_H #define LANDEVICELINK_H #include +#include #include #include #include #include #include "backends/devicelink.h" #include "uploadjob.h" +#include "compositeuploadjob.h" class SocketLineReader; class KDECONNECTCORE_EXPORT LanDeviceLink : public DeviceLink { Q_OBJECT public: enum ConnectionStarted : bool { Locally, Remotely }; LanDeviceLink(const QString& deviceId, LinkProvider* parent, QSslSocket* socket, ConnectionStarted connectionSource); void reset(QSslSocket* socket, ConnectionStarted connectionSource); QString name() override; bool sendPacket(NetworkPacket& np) override; - UploadJob* sendPayload(const NetworkPacket& np); void userRequestsPair() override; void userRequestsUnpair() override; void setPairStatus(PairStatus status) override; bool linkShouldBeKeptAlive() override; QHostAddress hostAddress() const; private Q_SLOTS: void dataReceived(); private: SocketLineReader* m_socketLineReader; ConnectionStarted m_connectionSource; QHostAddress m_hostAddress; + QPointer m_compositeUploadJob; }; #endif diff --git a/core/backends/lan/uploadjob.cpp b/core/backends/lan/uploadjob.cpp index 1bd2d17d..21119b2f 100644 --- a/core/backends/lan/uploadjob.cpp +++ b/core/backends/lan/uploadjob.cpp @@ -1,176 +1,108 @@ /* * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "uploadjob.h" #include #include "lanlinkprovider.h" #include "kdeconnectconfig.h" #include "core_debug.h" -#include #include -UploadJob::UploadJob(const QSharedPointer& source, const QString& deviceId) +UploadJob::UploadJob(const NetworkPacket& networkPacket) : KJob() - , m_input(source) - , m_server(new Server(this)) + , m_networkPacket(networkPacket) + , m_input(networkPacket.payload()) , m_socket(nullptr) - , m_port(0) - , m_deviceId(deviceId) // We will use this info if link is on ssl, to send encrypted payload { - connect(m_input.data(), &QIODevice::aboutToClose, this, &UploadJob::aboutToClose); - setCapabilities(Killable); } -void UploadJob::start() +void UploadJob::setSocket(QSslSocket* socket) { - m_port = MIN_PORT; - while (!m_server->listen(QHostAddress::Any, m_port)) { - m_port++; - if (m_port > MAX_PORT) { //No ports available? - qCWarning(KDECONNECT_CORE) << "Error opening a port in range" << MIN_PORT << "-" << MAX_PORT; - m_port = 0; - setError(1); - setErrorText(i18n("Couldn't find an available port")); - emitResult(); - return; - } - } - connect(m_server, &QTcpServer::newConnection, this, &UploadJob::newConnection); - - Q_EMIT description(this, i18n("Sending file to %1", Daemon::instance()->getDevice(this->m_deviceId)->name()), - { i18nc("File transfer origin", "From"), m_input.staticCast().data()->fileName() } - ); + m_socket = socket; + m_socket->setParent(this); } -void UploadJob::newConnection() +void UploadJob::start() { - if (!m_input->open(QIODevice::ReadOnly)) { + if (!m_input->open(QIODevice::ReadOnly)) { qCWarning(KDECONNECT_CORE) << "error when opening the input to upload"; return; //TODO: Handle error, clean up... } + + if (!m_socket) { + qCWarning(KDECONNECT_CORE) << "you must call setSocket() before calling start()"; + return; + } - m_socket = m_server->nextPendingConnection(); - m_socket->setParent(this); - connect(m_socket, &QSslSocket::disconnected, this, &UploadJob::cleanup); - connect(m_socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(socketFailed(QAbstractSocket::SocketError))); - connect(m_socket, SIGNAL(sslErrors(QList)), this, SLOT(sslErrors(QList))); - connect(m_socket, &QSslSocket::encrypted, this, &UploadJob::startUploading); - - LanLinkProvider::configureSslSocket(m_socket, m_deviceId, true); - - m_socket->startServerEncryption(); -} - -void UploadJob::startUploading() -{ + connect(m_input.data(), &QIODevice::aboutToClose, this, &UploadJob::aboutToClose); + bytesUploaded = 0; setProcessedAmount(Bytes, bytesUploaded); - setTotalAmount(Bytes, m_input.data()->size()); connect(m_socket, &QSslSocket::encryptedBytesWritten, this, &UploadJob::encryptedBytesWritten); - - if (!m_timer.isValid()) { - m_timer.start(); - } - + uploadNextPacket(); } void UploadJob::uploadNextPacket() { qint64 bytesAvailable = m_input->bytesAvailable(); if ( bytesAvailable > 0) { qint64 bytesToSend = qMin(m_input->bytesAvailable(), (qint64)4096); bytesUploading = m_socket->write(m_input->read(bytesToSend)); - - if (bytesUploading < 0) { - qCWarning(KDECONNECT_CORE) << "error when writing data to upload" << bytesToSend << m_input->bytesAvailable(); - } } if (bytesAvailable <= 0 || bytesUploading < 0) { m_input->close(); disconnect(m_socket, &QSslSocket::encryptedBytesWritten, this, &UploadJob::encryptedBytesWritten); } } void UploadJob::encryptedBytesWritten(qint64 bytes) { Q_UNUSED(bytes); - - bytesUploaded += bytesUploading; - + if (m_socket->encryptedBytesToWrite() == 0) { + bytesUploaded += bytesUploading; setProcessedAmount(Bytes, bytesUploaded); - const auto elapsed = m_timer.elapsed(); - if (elapsed > 0) { - emitSpeed((1000 * bytesUploaded) / elapsed); - } - uploadNextPacket(); } } void UploadJob::aboutToClose() { - qWarning() << "aboutToClose()"; m_socket->disconnectFromHost(); -} - -void UploadJob::cleanup() -{ - qWarning() << "cleanup()"; - m_socket->close(); emitResult(); } -QVariantMap UploadJob::transferInfo() -{ - Q_ASSERT(m_port != 0); - return {{"port", m_port}}; -} - -void UploadJob::socketFailed(QAbstractSocket::SocketError error) -{ - qWarning() << "socketFailed() " << error; - m_socket->close(); - setError(2); - emitResult(); +bool UploadJob::stop() { + m_input->close(); + + return true; } -void UploadJob::sslErrors(const QList& errors) +const NetworkPacket UploadJob::getNetworkPacket() { - qWarning() << "sslErrors() " << errors; - setError(1); - emitResult(); - m_socket->close(); -} - -bool UploadJob::doKill() -{ - if (m_input) { - m_input->close(); - } - return true; + return m_networkPacket; } diff --git a/core/backends/lan/uploadjob.h b/core/backends/lan/uploadjob.h index 7f9ff4f6..996b4deb 100644 --- a/core/backends/lan/uploadjob.h +++ b/core/backends/lan/uploadjob.h @@ -1,73 +1,61 @@ /* * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef UPLOADJOB_H #define UPLOADJOB_H #include #include #include -#include #include #include "server.h" #include -#include +#include class KDECONNECTCORE_EXPORT UploadJob : public KJob { Q_OBJECT public: - explicit UploadJob(const QSharedPointer& source, const QString& deviceId); + explicit UploadJob(const NetworkPacket& networkPacket); + void setSocket(QSslSocket* socket); void start() override; - - QVariantMap transferInfo(); + bool stop(); + const NetworkPacket getNetworkPacket(); private: - const QSharedPointer m_input; - Server * const m_server; + const NetworkPacket m_networkPacket; + QSharedPointer m_input; QSslSocket* m_socket; - quint16 m_port; - const QString m_deviceId; qint64 bytesUploading; qint64 bytesUploaded; - QElapsedTimer m_timer; const static quint16 MIN_PORT = 1739; const static quint16 MAX_PORT = 1764; -protected: - bool doKill() override; - private Q_SLOTS: - void startUploading(); void uploadNextPacket(); - void newConnection(); void encryptedBytesWritten(qint64 bytes); void aboutToClose(); - void cleanup(); - - void socketFailed(QAbstractSocket::SocketError); - void sslErrors(const QList& errors); }; #endif // UPLOADJOB_H diff --git a/plugins/share/shareplugin.cpp b/plugins/share/shareplugin.cpp index bc0503a9..2e4fe307 100644 --- a/plugins/share/shareplugin.cpp +++ b/plugins/share/shareplugin.cpp @@ -1,205 +1,211 @@ /** * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "shareplugin.h" #include "share_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include "core/filetransferjob.h" K_PLUGIN_FACTORY_WITH_JSON( KdeConnectPluginFactory, "kdeconnect_share.json", registerPlugin< SharePlugin >(); ) Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_SHARE, "kdeconnect.plugin.share") SharePlugin::SharePlugin(QObject* parent, const QVariantList& args) : KdeConnectPlugin(parent, args) { } QUrl SharePlugin::destinationDir() const { const QString defaultDownloadPath = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); QUrl dir = QUrl::fromLocalFile(config()->get(QStringLiteral("incoming_path"), defaultDownloadPath)); if (dir.path().contains(QLatin1String("%1"))) { dir.setPath(dir.path().arg(device()->name())); } KJob* job = KIO::mkpath(dir); bool ret = job->exec(); if (!ret) { qWarning() << "couldn't create" << dir; } return dir; } QUrl SharePlugin::getFileDestination(const QString filename) const { const QUrl dir = destinationDir().adjusted(QUrl::StripTrailingSlash); QUrl destination(dir); destination.setPath(dir.path() + '/' + filename, QUrl::DecodedMode); if (destination.isLocalFile() && QFile::exists(destination.toLocalFile())) { destination.setPath(dir.path() + '/' + KIO::suggestName(dir, filename), QUrl::DecodedMode); } return destination; } static QString cleanFilename(const QString &filename) { int idx = filename.lastIndexOf(QLatin1Char('/')); return idx>=0 ? filename.mid(idx + 1) : filename; } bool SharePlugin::receivePacket(const NetworkPacket& np) { /* //TODO: Write a test like this if (np.type() == PACKET_TYPE_PING) { qCDebug(KDECONNECT_PLUGIN_SHARE) << "sending file" << (QDesktopServices::storageLocation(QDesktopServices::HomeLocation) + "/.bashrc"); NetworkPacket out(PACKET_TYPE_SHARE_REQUEST); out.set("filename", mDestinationDir + "itworks.txt"); AutoClosingQFile* file = new AutoClosingQFile(QDesktopServices::storageLocation(QDesktopServices::HomeLocation) + "/.bashrc"); //Test file to transfer out.setPayload(file, file->size()); device()->sendPacket(out); return true; } */ qCDebug(KDECONNECT_PLUGIN_SHARE) << "File transfer"; if (np.hasPayload() || np.has(QStringLiteral("filename"))) { // qCDebug(KDECONNECT_PLUGIN_SHARE) << "receiving file" << filename << "in" << dir << "into" << destination; const QString filename = cleanFilename(np.get(QStringLiteral("filename"), QString::number(QDateTime::currentMSecsSinceEpoch()))); QUrl destination = getFileDestination(filename); if (np.hasPayload()) { FileTransferJob* job = np.createPayloadTransferJob(destination); job->setOriginName(device()->name() + ": " + filename); connect(job, &KJob::result, this, &SharePlugin::finished); KIO::getJobTracker()->registerJob(job); job->start(); } else { QFile file(destination.toLocalFile()); file.open(QIODevice::WriteOnly); file.close(); } } else if (np.has(QStringLiteral("text"))) { QString text = np.get(QStringLiteral("text")); if (!QStandardPaths::findExecutable(QStringLiteral("kate")).isEmpty()) { QProcess* proc = new QProcess(); connect(proc, SIGNAL(finished(int)), proc, SLOT(deleteLater())); proc->start(QStringLiteral("kate"), QStringList(QStringLiteral("--stdin"))); proc->write(text.toUtf8()); proc->closeWriteChannel(); } else { QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.open(); tmpFile.write(text.toUtf8()); tmpFile.close(); const QString fileName = tmpFile.fileName(); Q_EMIT shareReceived(fileName); QDesktopServices::openUrl(QUrl::fromLocalFile(fileName)); } } else if (np.has(QStringLiteral("url"))) { QUrl url = QUrl::fromEncoded(np.get(QStringLiteral("url"))); QDesktopServices::openUrl(url); Q_EMIT shareReceived(url.toString()); } else { qCDebug(KDECONNECT_PLUGIN_SHARE) << "Error: Nothing attached!"; } return true; } void SharePlugin::finished(KJob* job) { FileTransferJob* ftjob = qobject_cast(job); if (ftjob && !job->error()) { Q_EMIT shareReceived(ftjob->destination().toString()); qCDebug(KDECONNECT_PLUGIN_SHARE) << "File transfer finished." << ftjob->destination(); } else { qCDebug(KDECONNECT_PLUGIN_SHARE) << "File transfer failed." << (ftjob ? ftjob->destination() : QUrl()); } } void SharePlugin::openDestinationFolder() { QDesktopServices::openUrl(destinationDir()); } void SharePlugin::shareUrl(const QUrl& url) { NetworkPacket packet(PACKET_TYPE_SHARE_REQUEST); if(url.isLocalFile()) { QSharedPointer ioFile(new QFile(url.toLocalFile())); packet.setPayload(ioFile, ioFile->size()); packet.set(QStringLiteral("filename"), QUrl(url).fileName()); } else { packet.set(QStringLiteral("url"), url.toString()); } sendPacket(packet); } +void SharePlugin::shareUrls(const QStringList& urls) { + for(const QString url : urls) { + shareUrl(QUrl(url)); + } +} + void SharePlugin::shareText(const QString& text) { NetworkPacket packet(PACKET_TYPE_SHARE_REQUEST); packet.set(QStringLiteral("text"), text); sendPacket(packet); } void SharePlugin::openFile(const QUrl& url) { NetworkPacket packet(PACKET_TYPE_SHARE_REQUEST); if(url.isLocalFile()) { QSharedPointer ioFile(new QFile(url.toLocalFile())); packet.setPayload(ioFile, ioFile->size()); packet.set(QStringLiteral("filename"), QUrl(url).fileName()); packet.set(QStringLiteral("open"), true); } sendPacket(packet); } QString SharePlugin::dbusPath() const { return "/modules/kdeconnect/devices/" + device()->id() + "/share"; } #include "shareplugin.moc" diff --git a/plugins/share/shareplugin.h b/plugins/share/shareplugin.h index 4cb55763..58c63459 100644 --- a/plugins/share/shareplugin.h +++ b/plugins/share/shareplugin.h @@ -1,62 +1,62 @@ /** * Copyright 2013 Albert Vaca * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef SHAREPLUGIN_H #define SHAREPLUGIN_H #include #include #define PACKET_TYPE_SHARE_REQUEST QStringLiteral("kdeconnect.share.request") class SharePlugin : public KdeConnectPlugin { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.share") public: explicit SharePlugin(QObject* parent, const QVariantList& args); ///Helper method, QDBus won't recognize QUrl Q_SCRIPTABLE void shareUrl(const QString& url) { shareUrl(QUrl(url)); } + Q_SCRIPTABLE void shareUrls(const QStringList& urls); Q_SCRIPTABLE void shareText(const QString& text); Q_SCRIPTABLE void openFile(const QString& file) { openFile(QUrl(file)); } bool receivePacket(const NetworkPacket& np) override; void connected() override {} QString dbusPath() const override; private Q_SLOTS: void finished(KJob*); void openDestinationFolder(); Q_SIGNALS: void shareReceived(const QString& url); private: void shareUrl(const QUrl& url); void openFile(const QUrl& url); - QUrl destinationDir() const; QUrl getFileDestination(const QString filename) const; }; #endif diff --git a/tests/sendfiletest.cpp b/tests/sendfiletest.cpp index fddb6cde..6eedbb9e 100644 --- a/tests/sendfiletest.cpp +++ b/tests/sendfiletest.cpp @@ -1,144 +1,147 @@ /** * Copyright 2015 Aleix Pol Gonzalez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include "core/daemon.h" #include "core/device.h" #include "core/kdeconnectplugin.h" #include #include "kdeconnect-version.h" #include "testdaemon.h" +#include +#include class TestSendFile : public QObject { Q_OBJECT public: TestSendFile() { QStandardPaths::setTestModeEnabled(true); m_daemon = new TestDaemon; } private Q_SLOTS: void testSend() { m_daemon->acquireDiscoveryMode(QStringLiteral("test")); Device* d = nullptr; const QList devicesList = m_daemon->devicesList(); for (Device* id : devicesList) { if (id->isReachable()) { if (!id->isTrusted()) id->requestPair(); d = id; } } m_daemon->releaseDiscoveryMode(QStringLiteral("test")); QVERIFY(d); QCOMPARE(d->isReachable(), true); QCOMPARE(d->isTrusted(), true); QByteArray content("12312312312313213123213123"); QTemporaryFile temp; temp.open(); temp.write(content); temp.close(); KdeConnectPlugin* plugin = d->plugin(QStringLiteral("kdeconnect_share")); QVERIFY(plugin); plugin->metaObject()->invokeMethod(plugin, "shareUrl", Q_ARG(QString, QUrl::fromLocalFile(temp.fileName()).toString())); QSignalSpy spy(plugin, SIGNAL(shareReceived(QString))); QVERIFY(spy.wait(2000)); QVariantList args = spy.takeFirst(); QUrl sentFile(args.first().toUrl()); QFile file(sentFile.toLocalFile()); QCOMPARE(file.size(), content.size()); QVERIFY(file.open(QIODevice::ReadOnly)); QCOMPARE(file.readAll(), content); } void testSslJobs() { const QString aFile = QFINDTESTDATA("sendfiletest.cpp"); const QString destFile = QDir::tempPath() + "/kdeconnect-test-sentfile"; QFile(destFile).remove(); const QString deviceId = KdeConnectConfig::instance()->deviceId() , deviceName = QStringLiteral("testdevice") , deviceType = KdeConnectConfig::instance()->deviceType(); KdeConnectConfig* kcc = KdeConnectConfig::instance(); kcc->addTrustedDevice(deviceId, deviceName, deviceType); kcc->setDeviceProperty(deviceId, QStringLiteral("certificate"), QString::fromLatin1(kcc->certificate().toPem())); // Using same certificate from kcc, instead of generating QSharedPointer f(new QFile(aFile)); - UploadJob* uj = new UploadJob(f, deviceId); - QSignalSpy spyUpload(uj, &KJob::result); - uj->start(); - - auto info = uj->transferInfo(); - info.insert(QStringLiteral("deviceId"), deviceId); - info.insert(QStringLiteral("size"), aFile.size()); + NetworkPacket np(PACKET_TYPE_SHARE_REQUEST); + np.setPayload(f, aFile.size()); + CompositeUploadJob* job = new CompositeUploadJob(deviceId, false); + UploadJob* uj = new UploadJob(np); + job->addSubjob(uj); + + QSignalSpy spyUpload(job, &KJob::result); + job->start(); f->open(QIODevice::ReadWrite); - FileTransferJob* ft = new FileTransferJob(f, uj->transferInfo()[QStringLiteral("size")].toInt(), QUrl::fromLocalFile(destFile)); + FileTransferJob* ft = new FileTransferJob(f, aFile.size(), QUrl::fromLocalFile(destFile)); QSignalSpy spyTransfer(ft, &KJob::result); ft->start(); QVERIFY(spyTransfer.count() || spyTransfer.wait(1000000000)); if (ft->error()) qWarning() << "fterror" << ft->errorString(); QCOMPARE(ft->error(), 0); // HACK | FIXME: Why does this break the test? //QCOMPARE(spyUpload.count(), 1); QFile resultFile(destFile), originFile(aFile); QVERIFY(resultFile.open(QIODevice::ReadOnly)); QVERIFY(originFile.open(QIODevice::ReadOnly)); const QByteArray resultContents = resultFile.readAll(), originContents = originFile.readAll(); QCOMPARE(resultContents.size(), originContents.size()); QCOMPARE(resultFile.readAll(), originFile.readAll()); } private: TestDaemon* m_daemon; }; QTEST_MAIN(TestSendFile); #include "sendfiletest.moc"