|
|
@@ -32,8 +32,10 @@
|
|
|
#include "account.h"
|
|
|
#include "capabilities.h"
|
|
|
|
|
|
+#include <QBitArray>
|
|
|
#include <QDebug>
|
|
|
#include <QUrl>
|
|
|
+#include <QMetaMethod>
|
|
|
#include <QMetaObject>
|
|
|
#include <QStringList>
|
|
|
#include <QScopedPointer>
|
|
|
@@ -56,6 +58,11 @@
|
|
|
// The second number should be changed when there are new features.
|
|
|
#define MIRALL_SOCKET_API_VERSION "1.0"
|
|
|
|
|
|
+#define DEBUG qDebug() << "SocketApi: "
|
|
|
+
|
|
|
+Q_DECLARE_METATYPE(OCC::SocketListener)
|
|
|
+
|
|
|
+
|
|
|
static inline QString removeTrailingSlash(QString path)
|
|
|
{
|
|
|
Q_ASSERT(path.endsWith(QLatin1Char('/')));
|
|
|
@@ -63,9 +70,89 @@ static inline QString removeTrailingSlash(QString path)
|
|
|
return path;
|
|
|
}
|
|
|
|
|
|
+static QString buildMessage(const QString& verb, const QString &path, const QString &status = QString::null )
|
|
|
+{
|
|
|
+ QString msg(verb);
|
|
|
+
|
|
|
+ if( !status.isEmpty() ) {
|
|
|
+ msg.append(QLatin1Char(':'));
|
|
|
+ msg.append(status);
|
|
|
+ }
|
|
|
+ if( !path.isEmpty() ) {
|
|
|
+ msg.append(QLatin1Char(':'));
|
|
|
+ QFileInfo fi(path);
|
|
|
+ msg.append(QDir::toNativeSeparators(fi.absoluteFilePath()));
|
|
|
+ }
|
|
|
+ return msg;
|
|
|
+}
|
|
|
+
|
|
|
namespace OCC {
|
|
|
|
|
|
-#define DEBUG qDebug() << "SocketApi: "
|
|
|
+class BloomFilter {
|
|
|
+ // Initialize with m=1024 bits and k=2 (high and low 16 bits of a qHash).
|
|
|
+ // For a client navigating in less than 100 directories, this gives us a probability less than (1-e^(-2*100/1024))^2 = 0.03147872136 false positives.
|
|
|
+ const static int NumBits = 1024;
|
|
|
+
|
|
|
+public:
|
|
|
+ BloomFilter() : hashBits(NumBits) { }
|
|
|
+
|
|
|
+ void storeHash(uint hash) {
|
|
|
+ hashBits.setBit((hash & 0xFFFF) % NumBits);
|
|
|
+ hashBits.setBit((hash >> 16) % NumBits);
|
|
|
+ }
|
|
|
+ bool isHashMaybeStored(uint hash) const {
|
|
|
+ return hashBits.testBit((hash & 0xFFFF) % NumBits)
|
|
|
+ && hashBits.testBit((hash >> 16) % NumBits);
|
|
|
+ }
|
|
|
+
|
|
|
+private:
|
|
|
+ QBitArray hashBits;
|
|
|
+};
|
|
|
+
|
|
|
+class SocketListener {
|
|
|
+public:
|
|
|
+ QIODevice* socket;
|
|
|
+
|
|
|
+ SocketListener(QIODevice* socket = nullptr) : socket(socket) { }
|
|
|
+
|
|
|
+ void sendMessage(const QString& message, bool doWait = false) const
|
|
|
+ {
|
|
|
+ DEBUG << "Sending message: " << message;
|
|
|
+ QString localMessage = message;
|
|
|
+ if( ! localMessage.endsWith(QLatin1Char('\n'))) {
|
|
|
+ localMessage.append(QLatin1Char('\n'));
|
|
|
+ }
|
|
|
+
|
|
|
+ QByteArray bytesToSend = localMessage.toUtf8();
|
|
|
+ qint64 sent = socket->write(bytesToSend);
|
|
|
+ if( doWait ) {
|
|
|
+ socket->waitForBytesWritten(1000);
|
|
|
+ }
|
|
|
+ if( sent != bytesToSend.length() ) {
|
|
|
+ qDebug() << "WARN: Could not send all data on socket for " << localMessage;
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ void sendMessageIfDirectoryMonitored(const QString& message, uint systemDirectoryHash) const
|
|
|
+ {
|
|
|
+ if (_monitoredDirectoriesBloomFilter.isHashMaybeStored(systemDirectoryHash))
|
|
|
+ sendMessage(message, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ void registerMonitoredDirectory(uint systemDirectoryHash)
|
|
|
+ {
|
|
|
+ _monitoredDirectoriesBloomFilter.storeHash(systemDirectoryHash);
|
|
|
+ }
|
|
|
+private:
|
|
|
+ BloomFilter _monitoredDirectoriesBloomFilter;
|
|
|
+};
|
|
|
+
|
|
|
+struct ListenerHasSocketPred {
|
|
|
+ QIODevice *socket;
|
|
|
+ ListenerHasSocketPred(QIODevice *socket) : socket(socket) { }
|
|
|
+ bool operator()(const SocketListener &listener) const { return listener.socket == socket; }
|
|
|
+};
|
|
|
|
|
|
SocketApi::SocketApi(QObject* parent)
|
|
|
: QObject(parent)
|
|
|
@@ -129,7 +216,7 @@ SocketApi::~SocketApi()
|
|
|
DEBUG << "dtor";
|
|
|
_localServer.close();
|
|
|
// All remaining sockets will be destroyed with _localServer, their parent
|
|
|
- Q_ASSERT(_listeners.isEmpty() || _listeners.first()->parent() == &_localServer);
|
|
|
+ Q_ASSERT(_listeners.isEmpty() || _listeners.first().socket->parent() == &_localServer);
|
|
|
_listeners.clear();
|
|
|
}
|
|
|
|
|
|
@@ -143,14 +230,16 @@ void SocketApi::slotNewConnection()
|
|
|
DEBUG << "New connection" << socket;
|
|
|
connect(socket, SIGNAL(readyRead()), this, SLOT(slotReadSocket()));
|
|
|
connect(socket, SIGNAL(disconnected()), this, SLOT(onLostConnection()));
|
|
|
+ connect(socket, SIGNAL(destroyed(QObject*)), this, SLOT(slotSocketDestroyed(QObject*)));
|
|
|
Q_ASSERT(socket->readAll().isEmpty());
|
|
|
|
|
|
- _listeners.append(socket);
|
|
|
+ _listeners.append(SocketListener(socket));
|
|
|
+ SocketListener &listener = _listeners.last();
|
|
|
|
|
|
foreach( Folder *f, FolderMan::instance()->map() ) {
|
|
|
if (f->canSync()) {
|
|
|
QString message = buildRegisterPathMessage(removeTrailingSlash(f->path()));
|
|
|
- sendMessage(socket, message);
|
|
|
+ listener.sendMessage(message);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -158,17 +247,20 @@ void SocketApi::slotNewConnection()
|
|
|
void SocketApi::onLostConnection()
|
|
|
{
|
|
|
DEBUG << "Lost connection " << sender();
|
|
|
-
|
|
|
- QIODevice* socket = qobject_cast<QIODevice*>(sender());
|
|
|
- _listeners.removeAll(socket);
|
|
|
- socket->deleteLater();
|
|
|
+ sender()->deleteLater();
|
|
|
}
|
|
|
|
|
|
+void SocketApi::slotSocketDestroyed(QObject* obj)
|
|
|
+{
|
|
|
+ QIODevice* socket = static_cast<QIODevice*>(obj);
|
|
|
+ _listeners.erase(std::remove_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket)), _listeners.end());
|
|
|
+}
|
|
|
|
|
|
void SocketApi::slotReadSocket()
|
|
|
{
|
|
|
QIODevice* socket = qobject_cast<QIODevice*>(sender());
|
|
|
Q_ASSERT(socket);
|
|
|
+ SocketListener *listener = &*std::find_if(_listeners.begin(), _listeners.end(), ListenerHasSocketPred(socket));
|
|
|
|
|
|
while(socket->canReadLine()) {
|
|
|
// Make sure to normalize the input from the socket to
|
|
|
@@ -181,7 +273,7 @@ void SocketApi::slotReadSocket()
|
|
|
|
|
|
QString argument = line.remove(0, command.length()+1);
|
|
|
if(indexOfMethod != -1) {
|
|
|
- staticMetaObject.method(indexOfMethod).invoke(this, Q_ARG(QString, argument), Q_ARG(QIODevice*, socket));
|
|
|
+ staticMetaObject.method(indexOfMethod).invoke(this, Q_ARG(QString, argument), Q_ARG(SocketListener*, listener));
|
|
|
} else {
|
|
|
DEBUG << "The command is not supported by this version of the client:" << command << "with argument:" << argument;
|
|
|
}
|
|
|
@@ -197,8 +289,8 @@ void SocketApi::slotRegisterPath( const QString& alias )
|
|
|
Folder *f = FolderMan::instance()->folder(alias);
|
|
|
if (f) {
|
|
|
QString message = buildRegisterPathMessage(removeTrailingSlash(f->path()));
|
|
|
- foreach(QIODevice *socket, _listeners) {
|
|
|
- sendMessage(socket, message);
|
|
|
+ foreach (auto &listener, _listeners) {
|
|
|
+ listener.sendMessage(message);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -212,7 +304,7 @@ void SocketApi::slotUnregisterPath( const QString& alias )
|
|
|
|
|
|
Folder *f = FolderMan::instance()->folder(alias);
|
|
|
if (f)
|
|
|
- broadcastMessage(QLatin1String("UNREGISTER_PATH"), removeTrailingSlash(f->path()), QString::null, true );
|
|
|
+ broadcastMessage(buildMessage(QLatin1String("UNREGISTER_PATH"), removeTrailingSlash(f->path()), QString::null), true);
|
|
|
|
|
|
_registeredAliases.remove(alias);
|
|
|
}
|
|
|
@@ -233,68 +325,41 @@ void SocketApi::slotUpdateFolderView(Folder *f)
|
|
|
f->syncResult().status() == SyncResult::SetupError ) {
|
|
|
|
|
|
QString rootPath = removeTrailingSlash(f->path());
|
|
|
- broadcastMessage(QLatin1String("STATUS"), rootPath,
|
|
|
- f->syncEngine().syncFileStatusTracker().fileStatus("").toSocketAPIString());
|
|
|
+ broadcastStatusPushMessage(rootPath, f->syncEngine().syncFileStatusTracker().fileStatus(""));
|
|
|
|
|
|
- broadcastMessage(QLatin1String("UPDATE_VIEW"), rootPath);
|
|
|
+ broadcastMessage(buildMessage(QLatin1String("UPDATE_VIEW"), rootPath));
|
|
|
} else {
|
|
|
qDebug() << "Not sending UPDATE_VIEW for" << f->alias() << "because status() is" << f->syncResult().status();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-void SocketApi::slotFileStatusChanged(const QString& systemFileName, SyncFileStatus fileStatus)
|
|
|
-{
|
|
|
- broadcastMessage(QLatin1String("STATUS"), systemFileName, fileStatus.toSocketAPIString());
|
|
|
-}
|
|
|
-
|
|
|
-void SocketApi::sendMessage(QIODevice *socket, const QString& message, bool doWait)
|
|
|
+void SocketApi::broadcastMessage(const QString& msg, bool doWait)
|
|
|
{
|
|
|
- DEBUG << "Sending message: " << message;
|
|
|
- QString localMessage = message;
|
|
|
- if( ! localMessage.endsWith(QLatin1Char('\n'))) {
|
|
|
- localMessage.append(QLatin1Char('\n'));
|
|
|
- }
|
|
|
-
|
|
|
- QByteArray bytesToSend = localMessage.toUtf8();
|
|
|
- qint64 sent = socket->write(bytesToSend);
|
|
|
- if( doWait ) {
|
|
|
- socket->waitForBytesWritten(1000);
|
|
|
- }
|
|
|
- if( sent != bytesToSend.length() ) {
|
|
|
- qDebug() << "WARN: Could not send all data on socket for " << localMessage;
|
|
|
+ foreach (auto &listener, _listeners) {
|
|
|
+ listener.sendMessage(msg, doWait);
|
|
|
}
|
|
|
-
|
|
|
}
|
|
|
|
|
|
-void SocketApi::broadcastMessage( const QString& verb, const QString& path, const QString& status, bool doWait )
|
|
|
+void SocketApi::broadcastStatusPushMessage(const QString& systemPath, SyncFileStatus fileStatus)
|
|
|
{
|
|
|
- QString msg(verb);
|
|
|
-
|
|
|
- if( !status.isEmpty() ) {
|
|
|
- msg.append(QLatin1Char(':'));
|
|
|
- msg.append(status);
|
|
|
- }
|
|
|
- if( !path.isEmpty() ) {
|
|
|
- msg.append(QLatin1Char(':'));
|
|
|
- QFileInfo fi(path);
|
|
|
- msg.append(QDir::toNativeSeparators(fi.absoluteFilePath()));
|
|
|
- }
|
|
|
-
|
|
|
- foreach(QIODevice *socket, _listeners) {
|
|
|
- sendMessage(socket, msg, doWait);
|
|
|
+ QString msg = buildMessage(QLatin1String("STATUS"), systemPath, fileStatus.toSocketAPIString());
|
|
|
+ Q_ASSERT(!systemPath.endsWith('/'));
|
|
|
+ uint directoryHash = qHash(systemPath.left(systemPath.lastIndexOf('/')));
|
|
|
+ foreach (auto &listener, _listeners) {
|
|
|
+ listener.sendMessageIfDirectoryMonitored(msg, directoryHash);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-void SocketApi::command_RETRIEVE_FOLDER_STATUS(const QString& argument, QIODevice* socket)
|
|
|
+void SocketApi::command_RETRIEVE_FOLDER_STATUS(const QString& argument, SocketListener* listener)
|
|
|
{
|
|
|
// This command is the same as RETRIEVE_FILE_STATUS
|
|
|
|
|
|
//qDebug() << Q_FUNC_INFO << argument;
|
|
|
- command_RETRIEVE_FILE_STATUS(argument, socket);
|
|
|
+ command_RETRIEVE_FILE_STATUS(argument, listener);
|
|
|
}
|
|
|
|
|
|
-void SocketApi::command_RETRIEVE_FILE_STATUS(const QString& argument, QIODevice* socket)
|
|
|
+void SocketApi::command_RETRIEVE_FILE_STATUS(const QString& argument, SocketListener* listener)
|
|
|
{
|
|
|
qDebug() << Q_FUNC_INFO << argument;
|
|
|
|
|
|
@@ -305,21 +370,26 @@ void SocketApi::command_RETRIEVE_FILE_STATUS(const QString& argument, QIODevice*
|
|
|
// this can happen in offline mode e.g.: nothing to worry about
|
|
|
statusString = QLatin1String("NOP");
|
|
|
} else {
|
|
|
- QString relativePath = QDir::cleanPath(argument).mid(syncFolder->cleanPath().length()+1);
|
|
|
- if( relativePath.endsWith(QLatin1Char('/')) ) {
|
|
|
- relativePath.truncate(relativePath.length()-1);
|
|
|
- qWarning() << "Removed trailing slash for directory: " << relativePath << "Status pushes won't have one.";
|
|
|
+ QString systemPath = QDir::cleanPath(argument);
|
|
|
+ if( systemPath.endsWith(QLatin1Char('/')) ) {
|
|
|
+ systemPath.truncate(systemPath.length()-1);
|
|
|
+ qWarning() << "Removed trailing slash for directory: " << systemPath << "Status pushes won't have one.";
|
|
|
}
|
|
|
- SyncFileStatus fileStatus = syncFolder->syncEngine().syncFileStatusTracker().fileStatus(relativePath);
|
|
|
+ // The user probably visited this directory in the file shell.
|
|
|
+ // Let the listener know that it should now send status pushes for sibblings of this file.
|
|
|
+ QString directory = systemPath.left(systemPath.lastIndexOf('/'));
|
|
|
+ listener->registerMonitoredDirectory(qHash(directory));
|
|
|
|
|
|
+ QString relativePath = systemPath.mid(syncFolder->cleanPath().length()+1);
|
|
|
+ SyncFileStatus fileStatus = syncFolder->syncEngine().syncFileStatusTracker().fileStatus(relativePath);
|
|
|
statusString = fileStatus.toSocketAPIString();
|
|
|
}
|
|
|
|
|
|
const QString message = QLatin1String("STATUS:") % statusString % QLatin1Char(':') % QDir::toNativeSeparators(argument);
|
|
|
- sendMessage(socket, message);
|
|
|
+ listener->sendMessage(message);
|
|
|
}
|
|
|
|
|
|
-void SocketApi::command_SHARE(const QString& localFile, QIODevice* socket)
|
|
|
+void SocketApi::command_SHARE(const QString& localFile, SocketListener* listener)
|
|
|
{
|
|
|
qDebug() << Q_FUNC_INFO << localFile;
|
|
|
|
|
|
@@ -329,16 +399,16 @@ void SocketApi::command_SHARE(const QString& localFile, QIODevice* socket)
|
|
|
if (!shareFolder) {
|
|
|
const QString message = QLatin1String("SHARE:NOP:")+QDir::toNativeSeparators(localFile);
|
|
|
// files that are not within a sync folder are not synced.
|
|
|
- sendMessage(socket, message);
|
|
|
+ listener->sendMessage(message);
|
|
|
} else if (!shareFolder->accountState()->isConnected()) {
|
|
|
const QString message = QLatin1String("SHARE:NOTCONNECTED:")+QDir::toNativeSeparators(localFile);
|
|
|
// if the folder isn't connected, don't open the share dialog
|
|
|
- sendMessage(socket, message);
|
|
|
+ listener->sendMessage(message);
|
|
|
} else if (!theme->linkSharing() && (
|
|
|
!theme->userGroupSharing() ||
|
|
|
shareFolder->accountState()->account()->serverVersionInt() < ((8 << 16) + (2 << 8)))) {
|
|
|
const QString message = QLatin1String("SHARE:NOP:")+QDir::toNativeSeparators(localFile);
|
|
|
- sendMessage(socket, message);
|
|
|
+ listener->sendMessage(message);
|
|
|
} else {
|
|
|
const QString localFileClean = QDir::cleanPath(localFile);
|
|
|
const QString file = localFileClean.mid(shareFolder->cleanPath().length()+1);
|
|
|
@@ -347,7 +417,7 @@ void SocketApi::command_SHARE(const QString& localFile, QIODevice* socket)
|
|
|
// Verify the file is on the server (to our knowledge of course)
|
|
|
if (fileStatus.tag() != SyncFileStatus::StatusUpToDate) {
|
|
|
const QString message = QLatin1String("SHARE:NOTSYNCED:")+QDir::toNativeSeparators(localFile);
|
|
|
- sendMessage(socket, message);
|
|
|
+ listener->sendMessage(message);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -356,7 +426,7 @@ void SocketApi::command_SHARE(const QString& localFile, QIODevice* socket)
|
|
|
// Can't share root folder
|
|
|
if (remotePath == "/") {
|
|
|
const QString message = QLatin1String("SHARE:CANNOTSHAREROOT:")+QDir::toNativeSeparators(localFile);
|
|
|
- sendMessage(socket, message);
|
|
|
+ listener->sendMessage(message);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -370,18 +440,18 @@ void SocketApi::command_SHARE(const QString& localFile, QIODevice* socket)
|
|
|
}
|
|
|
}
|
|
|
const QString message = QLatin1String("SHARE:OK:")+QDir::toNativeSeparators(localFile);
|
|
|
- sendMessage(socket, message);
|
|
|
+ listener->sendMessage(message);
|
|
|
|
|
|
emit shareCommandReceived(remotePath, localFileClean, allowReshare);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-void SocketApi::command_VERSION(const QString&, QIODevice* socket)
|
|
|
+void SocketApi::command_VERSION(const QString&, SocketListener* listener)
|
|
|
{
|
|
|
- sendMessage(socket, QLatin1String("VERSION:" MIRALL_VERSION_STRING ":" MIRALL_SOCKET_API_VERSION));
|
|
|
+ listener->sendMessage(QLatin1String("VERSION:" MIRALL_VERSION_STRING ":" MIRALL_SOCKET_API_VERSION));
|
|
|
}
|
|
|
|
|
|
-void SocketApi::command_SHARE_STATUS(const QString &localFile, QIODevice *socket)
|
|
|
+void SocketApi::command_SHARE_STATUS(const QString &localFile, SocketListener* listener)
|
|
|
{
|
|
|
qDebug() << Q_FUNC_INFO << localFile;
|
|
|
|
|
|
@@ -389,7 +459,7 @@ void SocketApi::command_SHARE_STATUS(const QString &localFile, QIODevice *socket
|
|
|
|
|
|
if (!shareFolder) {
|
|
|
const QString message = QLatin1String("SHARE_STATUS:NOP:")+QDir::toNativeSeparators(localFile);
|
|
|
- sendMessage(socket, message);
|
|
|
+ listener->sendMessage(message);
|
|
|
} else {
|
|
|
const QString file = QDir::cleanPath(localFile).mid(shareFolder->cleanPath().length()+1);
|
|
|
SyncFileStatus fileStatus = shareFolder->syncEngine().syncFileStatusTracker().fileStatus(file);
|
|
|
@@ -397,7 +467,7 @@ void SocketApi::command_SHARE_STATUS(const QString &localFile, QIODevice *socket
|
|
|
// Verify the file is on the server (to our knowledge of course)
|
|
|
if (fileStatus.tag() != SyncFileStatus::StatusUpToDate) {
|
|
|
const QString message = QLatin1String("SHARE_STATUS:NOTSYNCED:")+QDir::toNativeSeparators(localFile);
|
|
|
- sendMessage(socket, message);
|
|
|
+ listener->sendMessage(message);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -405,7 +475,7 @@ void SocketApi::command_SHARE_STATUS(const QString &localFile, QIODevice *socket
|
|
|
|
|
|
if (!capabilities.shareAPI()) {
|
|
|
const QString message = QLatin1String("SHARE_STATUS:DISABLED:")+QDir::toNativeSeparators(localFile);
|
|
|
- sendMessage(socket, message);
|
|
|
+ listener->sendMessage(message);
|
|
|
} else {
|
|
|
auto theme = Theme::instance();
|
|
|
QString available;
|
|
|
@@ -424,18 +494,18 @@ void SocketApi::command_SHARE_STATUS(const QString &localFile, QIODevice *socket
|
|
|
|
|
|
if (available.isEmpty()) {
|
|
|
const QString message = QLatin1String("SHARE_STATUS:DISABLED") + ":" + QDir::toNativeSeparators(localFile);
|
|
|
- sendMessage(socket, message);
|
|
|
+ listener->sendMessage(message);
|
|
|
} else {
|
|
|
const QString message = QLatin1String("SHARE_STATUS:") + available + ":" + QDir::toNativeSeparators(localFile);
|
|
|
- sendMessage(socket, message);
|
|
|
+ listener->sendMessage(message);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-void SocketApi::command_SHARE_MENU_TITLE(const QString &, QIODevice* socket)
|
|
|
+void SocketApi::command_SHARE_MENU_TITLE(const QString &, SocketListener* listener)
|
|
|
{
|
|
|
- sendMessage(socket, QLatin1String("SHARE_MENU_TITLE:") + tr("Share with %1", "parameter is ownCloud").arg(Theme::instance()->appNameGUI()));
|
|
|
+ listener->sendMessage(QLatin1String("SHARE_MENU_TITLE:") + tr("Share with %1", "parameter is ownCloud").arg(Theme::instance()->appNameGUI()));
|
|
|
}
|
|
|
|
|
|
QString SocketApi::buildRegisterPathMessage(const QString& path)
|