git repos / SagoImageBrowser

blame: src/thumbnailcache.cpp

normal view · raw

ac476ca8 Poul Sander 2026-05-18 16:47 1
#include "thumbnailcache.h"
ac476ca8 Poul Sander 2026-05-18 16:47 2
ac476ca8 Poul Sander 2026-05-18 16:47 3
#include <QCryptographicHash>
ac476ca8 Poul Sander 2026-05-18 16:47 4
#include <QDir>
ac476ca8 Poul Sander 2026-05-18 16:47 5
#include <QFile>
ac476ca8 Poul Sander 2026-05-18 16:47 6
#include <QFileInfo>
ac476ca8 Poul Sander 2026-05-18 16:47 7
#include <QMimeDatabase>
ac476ca8 Poul Sander 2026-05-18 16:47 8
#include <QStandardPaths>
ac476ca8 Poul Sander 2026-05-18 16:47 9
#include <QTemporaryFile>
ac476ca8 Poul Sander 2026-05-18 16:47 10
#include <QUrl>
ac476ca8 Poul Sander 2026-05-18 16:47 11
ac476ca8 Poul Sander 2026-05-18 16:47 12
namespace
ac476ca8 Poul Sander 2026-05-18 16:47 13
{
ac476ca8 Poul Sander 2026-05-18 16:47 14
constexpr const char *kSoftware = "SagoImageBrowser";
ac476ca8 Poul Sander 2026-05-18 16:47 15
ac476ca8 Poul Sander 2026-05-18 16:47 16
QFile::Permissions filePerms()
ac476ca8 Poul Sander 2026-05-18 16:47 17
{
ac476ca8 Poul Sander 2026-05-18 16:47 18
    return QFile::ReadOwner | QFile::WriteOwner;
ac476ca8 Poul Sander 2026-05-18 16:47 19
}
ac476ca8 Poul Sander 2026-05-18 16:47 20
ac476ca8 Poul Sander 2026-05-18 16:47 21
QFile::Permissions dirPerms()
ac476ca8 Poul Sander 2026-05-18 16:47 22
{
ac476ca8 Poul Sander 2026-05-18 16:47 23
    return QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner;
ac476ca8 Poul Sander 2026-05-18 16:47 24
}
ac476ca8 Poul Sander 2026-05-18 16:47 25
} // namespace
ac476ca8 Poul Sander 2026-05-18 16:47 26
ac476ca8 Poul Sander 2026-05-18 16:47 27
QString ThumbnailCache::cacheRoot()
ac476ca8 Poul Sander 2026-05-18 16:47 28
{
ac476ca8 Poul Sander 2026-05-18 16:47 29
    QString base = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
ac476ca8 Poul Sander 2026-05-18 16:47 30
    if (base.isEmpty())
ac476ca8 Poul Sander 2026-05-18 16:47 31
        base = QDir::homePath() + "/.cache";
ac476ca8 Poul Sander 2026-05-18 16:47 32
    return base + "/thumbnails";
ac476ca8 Poul Sander 2026-05-18 16:47 33
}
ac476ca8 Poul Sander 2026-05-18 16:47 34
ac476ca8 Poul Sander 2026-05-18 16:47 35
QString ThumbnailCache::subdirFor(Size size)
ac476ca8 Poul Sander 2026-05-18 16:47 36
{
ac476ca8 Poul Sander 2026-05-18 16:47 37
    const char *name = (size == Size::Large) ? "large" : "normal";
ac476ca8 Poul Sander 2026-05-18 16:47 38
    return cacheRoot() + "/" + name;
ac476ca8 Poul Sander 2026-05-18 16:47 39
}
ac476ca8 Poul Sander 2026-05-18 16:47 40
ac476ca8 Poul Sander 2026-05-18 16:47 41
QString ThumbnailCache::canonicalUri(const QString &absPath)
ac476ca8 Poul Sander 2026-05-18 16:47 42
{
ac476ca8 Poul Sander 2026-05-18 16:47 43
    // Per the spec, use the absolute file URI (percent-encoded).
ac476ca8 Poul Sander 2026-05-18 16:47 44
    return QUrl::fromLocalFile(absPath).toString(QUrl::FullyEncoded);
ac476ca8 Poul Sander 2026-05-18 16:47 45
}
ac476ca8 Poul Sander 2026-05-18 16:47 46
ac476ca8 Poul Sander 2026-05-18 16:47 47
QString ThumbnailCache::hashedName(const QString &canonicalUri)
ac476ca8 Poul Sander 2026-05-18 16:47 48
{
ac476ca8 Poul Sander 2026-05-18 16:47 49
    QByteArray hash = QCryptographicHash::hash(canonicalUri.toUtf8(),
ac476ca8 Poul Sander 2026-05-18 16:47 50
                                               QCryptographicHash::Md5)
ac476ca8 Poul Sander 2026-05-18 16:47 51
                          .toHex();
ac476ca8 Poul Sander 2026-05-18 16:47 52
    return QString::fromLatin1(hash) + ".png";
ac476ca8 Poul Sander 2026-05-18 16:47 53
}
ac476ca8 Poul Sander 2026-05-18 16:47 54
ac476ca8 Poul Sander 2026-05-18 16:47 55
bool ThumbnailCache::isInsideCache(const QString &absPath)
ac476ca8 Poul Sander 2026-05-18 16:47 56
{
ac476ca8 Poul Sander 2026-05-18 16:47 57
    QString abs = QFileInfo(absPath).absoluteFilePath();
ac476ca8 Poul Sander 2026-05-18 16:47 58
    QString root = QFileInfo(cacheRoot()).absoluteFilePath();
ac476ca8 Poul Sander 2026-05-18 16:47 59
    if (root.isEmpty())
ac476ca8 Poul Sander 2026-05-18 16:47 60
        return false;
ac476ca8 Poul Sander 2026-05-18 16:47 61
    return abs.startsWith(root + "/") || abs == root;
ac476ca8 Poul Sander 2026-05-18 16:47 62
}
ac476ca8 Poul Sander 2026-05-18 16:47 63
ac476ca8 Poul Sander 2026-05-18 16:47 64
QImage ThumbnailCache::load(const QString &sourcePath, Size size)
ac476ca8 Poul Sander 2026-05-18 16:47 65
{
ac476ca8 Poul Sander 2026-05-18 16:47 66
    QFileInfo info(sourcePath);
ac476ca8 Poul Sander 2026-05-18 16:47 67
    if (!info.exists() || !info.isFile())
ac476ca8 Poul Sander 2026-05-18 16:47 68
        return {};
ac476ca8 Poul Sander 2026-05-18 16:47 69
ac476ca8 Poul Sander 2026-05-18 16:47 70
    const QString uri = canonicalUri(info.absoluteFilePath());
ac476ca8 Poul Sander 2026-05-18 16:47 71
    const QString cachePath = subdirFor(size) + "/" + hashedName(uri);
ac476ca8 Poul Sander 2026-05-18 16:47 72
ac476ca8 Poul Sander 2026-05-18 16:47 73
    QImage img;
ac476ca8 Poul Sander 2026-05-18 16:47 74
    if (!img.load(cachePath, "PNG"))
ac476ca8 Poul Sander 2026-05-18 16:47 75
        return {};
ac476ca8 Poul Sander 2026-05-18 16:47 76
ac476ca8 Poul Sander 2026-05-18 16:47 77
    const qint64 mtime = info.lastModified().toSecsSinceEpoch();
ac476ca8 Poul Sander 2026-05-18 16:47 78
    bool ok = false;
ac476ca8 Poul Sander 2026-05-18 16:47 79
    const qint64 cachedMTime = img.text("Thumb::MTime").toLongLong(&ok);
ac476ca8 Poul Sander 2026-05-18 16:47 80
    if (!ok || cachedMTime != mtime)
ac476ca8 Poul Sander 2026-05-18 16:47 81
        return {};
ac476ca8 Poul Sander 2026-05-18 16:47 82
ac476ca8 Poul Sander 2026-05-18 16:47 83
    return img;
ac476ca8 Poul Sander 2026-05-18 16:47 84
}
ac476ca8 Poul Sander 2026-05-18 16:47 85
ac476ca8 Poul Sander 2026-05-18 16:47 86
void ThumbnailCache::save(const QString &sourcePath, Size size, const QImage &thumbImage)
ac476ca8 Poul Sander 2026-05-18 16:47 87
{
ac476ca8 Poul Sander 2026-05-18 16:47 88
    if (thumbImage.isNull())
ac476ca8 Poul Sander 2026-05-18 16:47 89
        return;
ac476ca8 Poul Sander 2026-05-18 16:47 90
ac476ca8 Poul Sander 2026-05-18 16:47 91
    QFileInfo info(sourcePath);
ac476ca8 Poul Sander 2026-05-18 16:47 92
    if (!info.exists() || !info.isFile())
ac476ca8 Poul Sander 2026-05-18 16:47 93
        return;
ac476ca8 Poul Sander 2026-05-18 16:47 94
ac476ca8 Poul Sander 2026-05-18 16:47 95
    const QString uri = canonicalUri(info.absoluteFilePath());
ac476ca8 Poul Sander 2026-05-18 16:47 96
    const QString subdir = subdirFor(size);
ac476ca8 Poul Sander 2026-05-18 16:47 97
    const QString finalPath = subdir + "/" + hashedName(uri);
ac476ca8 Poul Sander 2026-05-18 16:47 98
ac476ca8 Poul Sander 2026-05-18 16:47 99
    QDir dir;
ac476ca8 Poul Sander 2026-05-18 16:47 100
    if (!dir.mkpath(subdir))
ac476ca8 Poul Sander 2026-05-18 16:47 101
        return;
ac476ca8 Poul Sander 2026-05-18 16:47 102
    QFile::setPermissions(subdir, dirPerms());
ac476ca8 Poul Sander 2026-05-18 16:47 103
    QFile::setPermissions(cacheRoot(), dirPerms());
ac476ca8 Poul Sander 2026-05-18 16:47 104
ac476ca8 Poul Sander 2026-05-18 16:47 105
    QImage tagged = thumbImage;
ac476ca8 Poul Sander 2026-05-18 16:47 106
    tagged.setText("Thumb::URI", uri);
ac476ca8 Poul Sander 2026-05-18 16:47 107
    tagged.setText("Thumb::MTime",
ac476ca8 Poul Sander 2026-05-18 16:47 108
                   QString::number(info.lastModified().toSecsSinceEpoch()));
ac476ca8 Poul Sander 2026-05-18 16:47 109
    tagged.setText("Thumb::Size", QString::number(info.size()));
ac476ca8 Poul Sander 2026-05-18 16:47 110
    QMimeDatabase mimeDb;
ac476ca8 Poul Sander 2026-05-18 16:47 111
    QString mime = mimeDb.mimeTypeForFile(info).name();
ac476ca8 Poul Sander 2026-05-18 16:47 112
    if (!mime.isEmpty())
ac476ca8 Poul Sander 2026-05-18 16:47 113
        tagged.setText("Thumb::Mimetype", mime);
ac476ca8 Poul Sander 2026-05-18 16:47 114
    tagged.setText("Thumb::Image::Width", QString::number(thumbImage.width()));
ac476ca8 Poul Sander 2026-05-18 16:47 115
    tagged.setText("Thumb::Image::Height", QString::number(thumbImage.height()));
ac476ca8 Poul Sander 2026-05-18 16:47 116
    tagged.setText("Software", QString::fromLatin1(kSoftware));
ac476ca8 Poul Sander 2026-05-18 16:47 117
ac476ca8 Poul Sander 2026-05-18 16:47 118
    QTemporaryFile tmp(subdir + "/tmp_XXXXXX.png");
ac476ca8 Poul Sander 2026-05-18 16:47 119
    tmp.setAutoRemove(false);
ac476ca8 Poul Sander 2026-05-18 16:47 120
    if (!tmp.open())
ac476ca8 Poul Sander 2026-05-18 16:47 121
        return;
ac476ca8 Poul Sander 2026-05-18 16:47 122
    const QString tmpPath = tmp.fileName();
ac476ca8 Poul Sander 2026-05-18 16:47 123
    tmp.close();
ac476ca8 Poul Sander 2026-05-18 16:47 124
ac476ca8 Poul Sander 2026-05-18 16:47 125
    if (!tagged.save(tmpPath, "PNG"))
ac476ca8 Poul Sander 2026-05-18 16:47 126
    {
ac476ca8 Poul Sander 2026-05-18 16:47 127
        QFile::remove(tmpPath);
ac476ca8 Poul Sander 2026-05-18 16:47 128
        return;
ac476ca8 Poul Sander 2026-05-18 16:47 129
    }
ac476ca8 Poul Sander 2026-05-18 16:47 130
ac476ca8 Poul Sander 2026-05-18 16:47 131
    QFile::setPermissions(tmpPath, filePerms());
ac476ca8 Poul Sander 2026-05-18 16:47 132
ac476ca8 Poul Sander 2026-05-18 16:47 133
    // Atomic replace: remove existing target then rename.
ac476ca8 Poul Sander 2026-05-18 16:47 134
    QFile::remove(finalPath);
ac476ca8 Poul Sander 2026-05-18 16:47 135
    if (!QFile::rename(tmpPath, finalPath))
ac476ca8 Poul Sander 2026-05-18 16:47 136
        QFile::remove(tmpPath);
ac476ca8 Poul Sander 2026-05-18 16:47 137
}
1970-01-01 00:00 138