2 changed files with 104 additions and 0 deletions
@ -0,0 +1,89 @@
|
||||
#include "avatarcache.h" |
||||
#include <QBuffer> |
||||
#include <QCryptographicHash> |
||||
#include <QDateTime> |
||||
#include <QDir> |
||||
#include <QFile> |
||||
#include <QImageReader> |
||||
#include <QSaveFile> |
||||
extern "C" { |
||||
#include "member_files/protocol.h" |
||||
#include "debug_config.h" |
||||
} |
||||
|
||||
bool AvatarCache::initialize(const QString& directory) { |
||||
if (!QDir().mkpath(directory)) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "avatar: cannot create cache path=%s", qPrintable(directory)); return false; |
||||
} |
||||
const auto entries = QDir(directory).entryInfoList({QStringLiteral("*.part*"), QStringLiteral("*.download*")}, QDir::Files); |
||||
for (const auto& entry : entries) if (!QFile::remove(entry.absoluteFilePath())) |
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "avatar: cannot remove interrupted cache file=%s", qPrintable(entry.fileName())); |
||||
prune(directory, {}); |
||||
return true; |
||||
} |
||||
|
||||
AvatarCache::Result AvatarCache::load(const QString& path, const QString& hash) { |
||||
Result result; result.hash = hash; |
||||
QFile file(path); |
||||
if (!file.open(QIODevice::ReadOnly)) { result.error = QStringLiteral("missing cache file"); return result; } |
||||
if (file.size() <= 0 || file.size() > MaxBytes) { result.error = QStringLiteral("invalid avatar size"); return result; } |
||||
const QByteArray bytes = file.read(MaxBytes + 1); |
||||
if (bytes.size() > MaxBytes || QCryptographicHash::hash(bytes, QCryptographicHash::Sha256).toHex() != hash.toLatin1()) { |
||||
result.error = QStringLiteral("avatar hash mismatch"); return result; |
||||
} |
||||
QBuffer buffer; buffer.setData(bytes); buffer.open(QIODevice::ReadOnly); |
||||
QImageReader reader(&buffer, "png"); const QSize size = reader.size(); |
||||
if (!size.isValid() || size.width() > 512 || size.height() > 512 || size.width() != size.height()) { |
||||
result.error = QStringLiteral("invalid avatar dimensions"); return result; |
||||
} |
||||
result.image = reader.read(); |
||||
if (result.image.isNull()) { result.error = reader.errorString(); return result; } |
||||
file.setFileTime(QDateTime::currentDateTimeUtc(), QFileDevice::FileModificationTime); |
||||
return result; |
||||
} |
||||
|
||||
AvatarCache::Result AvatarCache::prepare(const QString& source, const QString& directory) { |
||||
Result result; QImageReader reader(source); reader.setAutoTransform(true); |
||||
const QSize size = reader.size(); |
||||
if (!size.isValid() || qint64(size.width()) * size.height() > 16 * 1024 * 1024 || reader.format() == "svg") { |
||||
result.error = QStringLiteral("image exceeds 16 megapixels or has an unsupported format"); return result; |
||||
} |
||||
QImage image = reader.read(); |
||||
if (image.isNull()) { result.error = reader.errorString(); return result; } |
||||
int side = qMin(image.width(), image.height()); |
||||
image = image.copy((image.width() - side) / 2, (image.height() - side) / 2, side, side) |
||||
.scaled(256, 256, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); |
||||
QByteArray bytes; QBuffer buffer(&bytes); buffer.open(QIODevice::WriteOnly); |
||||
if (!image.save(&buffer, "PNG") || bytes.size() > MaxBytes) { result.error = QStringLiteral("cannot encode avatar"); return result; } |
||||
result.hash = QString::fromLatin1(QCryptographicHash::hash(bytes, QCryptographicHash::Sha256).toHex()); |
||||
QSaveFile file(directory + '/' + result.hash + QStringLiteral(".png")); |
||||
if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) { |
||||
result.error = file.errorString(); return result; |
||||
} |
||||
result.image = image; return result; |
||||
} |
||||
|
||||
AvatarCache::Result AvatarCache::publish(const QString& temporary, const QString& directory, const QString& hash) { |
||||
Result result = load(temporary, hash); |
||||
if (!result.image.isNull()) { |
||||
QFile source(temporary); QSaveFile target(directory + '/' + hash + QStringLiteral(".png")); |
||||
if (!source.open(QIODevice::ReadOnly) || !target.open(QIODevice::WriteOnly) || |
||||
target.write(source.readAll()) != source.size() || !target.commit()) { |
||||
result.image = {}; result.error = QStringLiteral("cannot publish avatar cache"); |
||||
} |
||||
} |
||||
if (!QFile::remove(temporary)) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "avatar: temporary cleanup failed path=%s", qPrintable(temporary)); |
||||
return result; |
||||
} |
||||
|
||||
void AvatarCache::prune(const QString& directory, const QStringList& pinned) { |
||||
const auto files = QDir(directory).entryInfoList({QStringLiteral("*.png")}, QDir::Files, QDir::Time); |
||||
qint64 total = 0; for (const auto& file : files) total += file.size(); |
||||
for (int i = files.size() - 1; total > DiskLimit && i >= 0; i--) { |
||||
const auto& file = files[i]; |
||||
if (!mf_avatar_file_valid(file.fileName().toLatin1().constData()) || pinned.contains(file.baseName())) continue; |
||||
if (QFile::remove(file.absoluteFilePath())) { |
||||
total -= file.size(); DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "avatar: cache evicted file=%s", qPrintable(file.fileName())); |
||||
} else DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "avatar: cache eviction failed file=%s", qPrintable(file.fileName())); |
||||
} |
||||
} |
||||
@ -0,0 +1,15 @@
|
||||
/* Дисковый кеш неизменяемых аватарок. Все методы выполняются в worker. */ |
||||
#pragma once |
||||
#include <QImage> |
||||
#include <QString> |
||||
|
||||
namespace AvatarCache { |
||||
constexpr int MaxBytes = 1024 * 1024; |
||||
constexpr qint64 DiskLimit = 100 * 1024 * 1024; |
||||
struct Result { QImage image; QString hash, error; }; |
||||
bool initialize(const QString& directory); |
||||
Result load(const QString& path, const QString& hash); |
||||
Result prepare(const QString& source, const QString& directory); |
||||
Result publish(const QString& temporary, const QString& directory, const QString& hash); |
||||
void prune(const QString& directory, const QStringList& pinned); |
||||
} |
||||
Loading…
Reference in new issue