src/thumbnailcache.cpp
browsing at commit = ac476ca8eaff9af7ee53a93d48d8dd760f7d3a27
#include "thumbnailcache.h"
#include <QCryptographicHash>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QMimeDatabase>
#include <QStandardPaths>
#include <QTemporaryFile>
#include <QUrl>
namespace
{
constexpr const char *kSoftware = "SagoImageBrowser";
QFile::Permissions filePerms()
{
return QFile::ReadOwner | QFile::WriteOwner;
}
QFile::Permissions dirPerms()
{
return QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner;
}
} // namespace
QString ThumbnailCache::cacheRoot()
{
QString base = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
if (base.isEmpty())
base = QDir::homePath() + "/.cache";
return base + "/thumbnails";
}
QString ThumbnailCache::subdirFor(Size size)
{
const char *name = (size == Size::Large) ? "large" : "normal";
return cacheRoot() + "/" + name;
}
QString ThumbnailCache::canonicalUri(const QString &absPath)
{
// Per the spec, use the absolute file URI (percent-encoded).
return QUrl::fromLocalFile(absPath).toString(QUrl::FullyEncoded);
}
QString ThumbnailCache::hashedName(const QString &canonicalUri)
{
QByteArray hash = QCryptographicHash::hash(canonicalUri.toUtf8(),
QCryptographicHash::Md5)
.toHex();
return QString::fromLatin1(hash) + ".png";
}
bool ThumbnailCache::isInsideCache(const QString &absPath)
{
QString abs = QFileInfo(absPath).absoluteFilePath();
QString root = QFileInfo(cacheRoot()).absoluteFilePath();
if (root.isEmpty())
return false;
return abs.startsWith(root + "/") || abs == root;
}
QImage ThumbnailCache::load(const QString &sourcePath, Size size)
{
QFileInfo info(sourcePath);
if (!info.exists() || !info.isFile())
return {};
const QString uri = canonicalUri(info.absoluteFilePath());
const QString cachePath = subdirFor(size) + "/" + hashedName(uri);
QImage img;
if (!img.load(cachePath, "PNG"))
return {};
const qint64 mtime = info.lastModified().toSecsSinceEpoch();
bool ok = false;
const qint64 cachedMTime = img.text("Thumb::MTime").toLongLong(&ok);
if (!ok || cachedMTime != mtime)
return {};
return img;
}
void ThumbnailCache::save(const QString &sourcePath, Size size, const QImage &thumbImage)
{
if (thumbImage.isNull())
return;
QFileInfo info(sourcePath);
if (!info.exists() || !info.isFile())
return;
const QString uri = canonicalUri(info.absoluteFilePath());
const QString subdir = subdirFor(size);
const QString finalPath = subdir + "/" + hashedName(uri);
QDir dir;
if (!dir.mkpath(subdir))
return;
QFile::setPermissions(subdir, dirPerms());
QFile::setPermissions(cacheRoot(), dirPerms());
QImage tagged = thumbImage;
tagged.setText("Thumb::URI", uri);
tagged.setText("Thumb::MTime",
QString::number(info.lastModified().toSecsSinceEpoch()));
tagged.setText("Thumb::Size", QString::number(info.size()));
QMimeDatabase mimeDb;
QString mime = mimeDb.mimeTypeForFile(info).name();
if (!mime.isEmpty())
tagged.setText("Thumb::Mimetype", mime);
tagged.setText("Thumb::Image::Width", QString::number(thumbImage.width()));
tagged.setText("Thumb::Image::Height", QString::number(thumbImage.height()));
tagged.setText("Software", QString::fromLatin1(kSoftware));
QTemporaryFile tmp(subdir + "/tmp_XXXXXX.png");
tmp.setAutoRemove(false);
if (!tmp.open())
return;
const QString tmpPath = tmp.fileName();
tmp.close();
if (!tagged.save(tmpPath, "PNG"))
{
QFile::remove(tmpPath);
return;
}
QFile::setPermissions(tmpPath, filePerms());
// Atomic replace: remove existing target then rename.
QFile::remove(finalPath);
if (!QFile::rename(tmpPath, finalPath))
QFile::remove(tmpPath);
}