#include "account.h"
#include "capabilities.h"
+#include <QBitArray>
#include <QDebug>
#include <QUrl>
+#include <QMetaMethod>
#include <QMetaObject>
#include <QStringList>
#include <QScopedPointer>
// 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('/')));
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)
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();
}
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);
}
}
}
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
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;
}
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);
}
}
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);
}
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;
// 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;
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);
// 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;
}
// Can't share root folder
if (remotePath == "/") {
const QString message = QLatin1String("SHARE:CANNOTSHAREROOT:")+QDir::toNativeSeparators(localFile);
- sendMessage(socket, message);
+ listener->sendMessage(message);
return;
}
}
}
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;
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);
// 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;
}
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;
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)