1
0
forked from blue/squawk
squawk/core/storage/archive.cpp

421 lines
12 KiB
C++

/*
* Squawk messenger.
* Copyright (C) 2019 Yury Gubich <blue@macaw.me>
*
* 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 3 of the License, or
* (at your option) any later version.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "archive.h"
#include <sys/stat.h>
#include <sys/types.h>
#include <QStandardPaths>
#include <QDebug>
#include <QDataStream>
#include <QDir>
Core::Archive::Archive(const QString& account, const QString& p_jid, QObject* parent):
QObject(parent),
jid(p_jid),
account(account),
opened(false),
db(account + "/" + jid),
messages(db.addStorage<QString, Shared::Message>("messages")),
order(db.addStorage<uint64_t, QString>("order")),
stats(db.addStorage<QString, QVariant>("stats")),
avatars(db.addStorage<QString, AvatarInfo>("avatars")),
stanzaIdToId(db.addStorage<QString, QString>("stanzaIdToId")),
cursor(order->createCursor())
{}
Core::Archive::~Archive() {
close();
}
void Core::Archive::open() {
db.open();
LMDBAL::WriteTransaction txn = db.beginTransaction();
AvatarInfo info;
bool hasAvatar = false;
try {
avatars->getRecord(jid, info, txn);
hasAvatar = true;
} catch (const LMDBAL::NotFound& e) {}
if (!hasAvatar)
return;
QFile ava(db.getPath() + "/" + jid + "." + info.type);
if (ava.exists())
return;
try {
avatars->removeRecord(jid, txn);
txn.commit();
} catch (const std::exception& e) {
qDebug() << e.what();
qDebug() << "error opening archive" << jid << "for account" << account
<< ". There is supposed to be avatar but the file doesn't exist, couldn't even drop it, it surely will lead to an error";
}
}
void Core::Archive::close() {
db.close();
}
bool Core::Archive::addElement(const Shared::Message& message) {
QString id = message.getId();
qDebug() << "Adding message with id " << id;
try {
LMDBAL::WriteTransaction txn = db.beginTransaction();
messages->addRecord(id, message, txn);
order->addRecord(message.getTime().toMSecsSinceEpoch(), id, txn);
QString stanzaId = message.getStanzaId();
if (!stanzaId.isEmpty())
stanzaIdToId->addRecord(stanzaId, id);
txn.commit();
return true;
} catch (const std::exception& e) {
qDebug() << "Could not add message with id " + id;
qDebug() << e.what();
}
return false;
}
void Core::Archive::clear() {
db.drop();
}
Shared::Message Core::Archive::getElement(const QString& id) const {
return messages->getRecord(id);
}
bool Core::Archive::hasElement(const QString& id) const {
return messages->checkRecord(id);
}
void Core::Archive::changeMessage(const QString& id, const QMap<QString, QVariant>& data) {
LMDBAL::WriteTransaction txn = db.beginTransaction();
Shared::Message msg = messages->getRecord(id, txn);
bool hadStanzaId = !msg.getStanzaId().isEmpty();
QDateTime oTime = msg.getTime();
bool idChange = msg.change(data);
QString newId = msg.getId();
QDateTime nTime = msg.getTime();
bool orderChange = oTime != nTime;
if (idChange || orderChange) {
if (idChange)
messages->removeRecord(id, txn);
if (orderChange)
order->removeRecord(oTime.toMSecsSinceEpoch(), txn);
order->forceRecord(nTime.toMSecsSinceEpoch(), newId, txn);
}
QString sid = msg.getStanzaId();
if (!sid.isEmpty() && (idChange || !hadStanzaId))
stanzaIdToId->forceRecord(sid, newId, txn);
messages->forceRecord(newId, msg, txn);
txn.commit();
}
Shared::Message Core::Archive::newest() const {
LMDBAL::Transaction txn = db.beginReadOnlyTransaction();
try {
cursor.open(txn);
while (true) {
std::pair<uint64_t, QString> pair = cursor.prev();
Shared::Message msg = messages->getRecord(pair.second, txn);
if (msg.serverStored()) {
cursor.close();
return msg;
}
}
} catch (...) {
cursor.close();
throw;
}
}
QString Core::Archive::newestId() const {
Shared::Message msg = newest();
return msg.getId();
}
QString Core::Archive::oldestId() const {
Shared::Message msg = oldest();
return msg.getId();
}
Shared::Message Core::Archive::oldest() const {
LMDBAL::Transaction txn = db.beginReadOnlyTransaction();
try {
cursor.open(txn);
while (true) {
std::pair<uint64_t, QString> pair = cursor.next();
Shared::Message msg = messages->getRecord(pair.second, txn);
if (msg.serverStored()) {
cursor.close();
return msg;
}
}
} catch (...) {
cursor.close();
throw;
}
}
unsigned int Core::Archive::addElements(const std::list<Shared::Message>& messages) {
unsigned int success = 0;
LMDBAL::WriteTransaction txn = db.beginTransaction();
for (const Shared::Message& message : messages) {
QString id = message.getId();
bool added = false;
try {
Core::Archive::messages->addRecord(id, message, txn);
added = true;
} catch (const LMDBAL::Exist& e) {}
if (!added)
continue;
order->addRecord(message.getTime().toMSecsSinceEpoch(), id);
QString sid = message.getStanzaId();
if (!sid.isEmpty())
stanzaIdToId->addRecord(sid, id);
++success;
}
return success;
}
long unsigned int Core::Archive::size() const {
return order->count();
}
std::list<Shared::Message> Core::Archive::getBefore(unsigned int count, const QString& id) {
LMDBAL::Transaction txn = db.beginReadOnlyTransaction();
try {
cursor.open(txn);
if (id.isEmpty()) {
cursor.last();
} else {
Shared::Message reference = messages->getRecord(id, txn);
uint64_t stamp = reference.getTime().toMSecsSinceEpoch();
cursor.set(stamp);
cursor.prev();
}
std::list<Shared::Message> res;
for (unsigned int i = 0; i < count; ++i) {
std::pair<uint64_t, QString> pair;
if (i == 0)
cursor.current(pair.first, pair.second);
else
cursor.prev(pair.first, pair.second);
res.emplace_back();
Shared::Message& msg = res.back();
messages->getRecord(pair.second, msg, txn);
}
cursor.close();
return res;
} catch (...) {
cursor.close();
throw;
}
}
bool Core::Archive::isFromTheBeginning() const {
try {
return stats->getRecord("fromTheBeginning").toBool();
} catch (const LMDBAL::NotFound& e) {
return false;
}
}
void Core::Archive::setFromTheBeginning(bool is) {
stats->forceRecord("fromTheBeginning", is);
}
bool Core::Archive::isEncryptionEnabled() const {
try {
return stats->getRecord("isEncryptionEnabled").toBool();
} catch (const LMDBAL::NotFound& e) {
return false;
}
}
bool Core::Archive::setEncryptionEnabled(bool is) {
LMDBAL::WriteTransaction txn = db.beginTransaction();
bool current = false;
try {
current = stats->getRecord("isEncryptionEnabled").toBool();
} catch (const LMDBAL::NotFound& e) {}
if (is != current) {
stats->forceRecord("isEncryptionEnabled", is, txn);
txn.commit();
return true;
}
return false;
}
QString Core::Archive::idByStanzaId(const QString& stanzaId) const {
return stanzaIdToId->getRecord(stanzaId);
}
QString Core::Archive::stanzaIdById(const QString& id) const {
try {
Shared::Message msg = getElement(id);
return msg.getStanzaId();
} catch (const LMDBAL::NotFound& e) {
return QString();
}
}
bool Core::Archive::setAvatar(const QByteArray& data, AvatarInfo& newInfo, bool generated, const QString& resource) {
LMDBAL::WriteTransaction txn = db.beginTransaction();
AvatarInfo oldInfo;
bool haveAvatar = false;
QString res = resource.isEmpty() ? jid : resource;
try {
avatars->getRecord(res, oldInfo, txn);
haveAvatar = true;
} catch (const LMDBAL::NotFound& e) {}
if (data.size() == 0) {
if (!haveAvatar)
return false;
avatars->removeRecord(res, txn);
txn.commit();
return true;
} else {
QString currentPath = db.getPath();
bool needToRemoveOld = false;
QCryptographicHash hash(QCryptographicHash::Sha1);
hash.addData(data);
QByteArray newHash(hash.result());
if (haveAvatar) {
if (!generated && !oldInfo.autogenerated && oldInfo.hash == newHash)
return false;
QFile oldAvatar(currentPath + "/" + res + "." + oldInfo.type);
if (oldAvatar.exists()) {
if (oldAvatar.rename(currentPath + "/" + res + "." + oldInfo.type + ".bak")) {
needToRemoveOld = true;
} else {
qDebug() << "Can't change avatar: couldn't get rid of the old avatar" << oldAvatar.fileName();
return false;
}
}
}
QMimeDatabase mimedb;
QMimeType type = mimedb.mimeTypeForData(data);
QString ext = type.preferredSuffix();
QFile newAvatar(currentPath + "/" + res + "." + ext);
if (newAvatar.open(QFile::WriteOnly)) {
newAvatar.write(data);
newAvatar.close();
newInfo.type = ext;
newInfo.hash = newHash;
newInfo.autogenerated = generated;
try {
avatars->forceRecord(res, newInfo, txn);
txn.commit();
} catch (...) {
qDebug() << "Can't change avatar: couldn't store changes to database for" << newAvatar.fileName() << "rolling back to the previous state";
if (needToRemoveOld) {
QFile oldAvatar(currentPath + "/" + res + "." + oldInfo.type + ".bak");
oldAvatar.rename(currentPath + "/" + res + "." + oldInfo.type);
}
return false;
}
if (needToRemoveOld) {
QFile oldAvatar(currentPath + "/" + res + "." + oldInfo.type + ".bak");
oldAvatar.remove();
}
return true;
} else {
qDebug() << "Can't change avatar: cant open file to write" << newAvatar.fileName() << "rolling back to the previous state";
if (needToRemoveOld) {
QFile oldAvatar(currentPath + "/" + res + "." + oldInfo.type + ".bak");
oldAvatar.rename(currentPath + "/" + res + "." + oldInfo.type);
}
return false;
}
}
}
bool Core::Archive::readAvatarInfo(Core::Archive::AvatarInfo& target, const QString& resource) const {
try {
avatars->getRecord(resource.isEmpty() ? jid : resource, target);
return true;
} catch (const LMDBAL::NotFound& e) {
return false;
}
}
void Core::Archive::readAllResourcesAvatars(std::map<QString, AvatarInfo>& data) const {
avatars->readAll(data);
}
Core::Archive::AvatarInfo Core::Archive::getAvatarInfo(const QString& resource) const {
return avatars->getRecord(resource);
}
Core::Archive::AvatarInfo::AvatarInfo():
type(),
hash(),
autogenerated(false)
{}
Core::Archive::AvatarInfo::AvatarInfo(const QString& p_type, const QByteArray& p_hash, bool p_autogenerated):
type(p_type),
hash(p_hash),
autogenerated(p_autogenerated)
{}
QDataStream & operator<<(QDataStream& out, const Core::Archive::AvatarInfo& info) {
out << info.type;
out << info.hash;
out << info.autogenerated;
return out;
}
QDataStream & operator>>(QDataStream& in, Core::Archive::AvatarInfo& info) {
in >> info.type;
in >> info.hash;
in >> info.autogenerated;
return in;
}