git repos / SagoImageBrowser

blame: src/thumbnailcache.cpp

normal view · raw

14cf904f Poul Sander 2026-06-22 18:18 1
/*
14cf904f Poul Sander 2026-06-22 18:18 2
MIT License
14cf904f Poul Sander 2026-06-22 18:18 3
14cf904f Poul Sander 2026-06-22 18:18 4
Copyright (c) 2026 Poul Sander
14cf904f Poul Sander 2026-06-22 18:18 5
14cf904f Poul Sander 2026-06-22 18:18 6
Permission is hereby granted, free of charge, to any person obtaining a copy
14cf904f Poul Sander 2026-06-22 18:18 7
of this software and associated documentation files (the "Software"), to deal
14cf904f Poul Sander 2026-06-22 18:18 8
in the Software without restriction, including without limitation the rights
14cf904f Poul Sander 2026-06-22 18:18 9
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14cf904f Poul Sander 2026-06-22 18:18 10
copies of the Software, and to permit persons to whom the Software is
14cf904f Poul Sander 2026-06-22 18:18 11
furnished to do so, subject to the following conditions:
14cf904f Poul Sander 2026-06-22 18:18 12
14cf904f Poul Sander 2026-06-22 18:18 13
The above copyright notice and this permission notice shall be included in all
14cf904f Poul Sander 2026-06-22 18:18 14
copies or substantial portions of the Software.
14cf904f Poul Sander 2026-06-22 18:18 15
14cf904f Poul Sander 2026-06-22 18:18 16
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14cf904f Poul Sander 2026-06-22 18:18 17
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14cf904f Poul Sander 2026-06-22 18:18 18
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14cf904f Poul Sander 2026-06-22 18:18 19
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
14cf904f Poul Sander 2026-06-22 18:18 20
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
14cf904f Poul Sander 2026-06-22 18:18 21
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
14cf904f Poul Sander 2026-06-22 18:18 22
SOFTWARE.
14cf904f Poul Sander 2026-06-22 18:18 23
14cf904f Poul Sander 2026-06-22 18:18 24
*/
14cf904f Poul Sander 2026-06-22 18:18 25
ac476ca8 Poul Sander 2026-05-18 16:47 26
#include "thumbnailcache.h"
ac476ca8 Poul Sander 2026-05-18 16:47 27
ac476ca8 Poul Sander 2026-05-18 16:47 28
#include <QCryptographicHash>
ac476ca8 Poul Sander 2026-05-18 16:47 29
#include <QDir>
ac476ca8 Poul Sander 2026-05-18 16:47 30
#include <QFile>
ac476ca8 Poul Sander 2026-05-18 16:47 31
#include <QStandardPaths>
ac476ca8 Poul Sander 2026-05-18 16:47 32
#include <QTemporaryFile>
ac476ca8 Poul Sander 2026-05-18 16:47 33
#include <QUrl>
ac476ca8 Poul Sander 2026-05-18 16:47 34
80a9926d Poul Sander 2026-05-19 20:28 35
#include <sys/stat.h>
80a9926d Poul Sander 2026-05-19 20:28 36
ac476ca8 Poul Sander 2026-05-18 16:47 37
namespace
ac476ca8 Poul Sander 2026-05-18 16:47 38
{
ac476ca8 Poul Sander 2026-05-18 16:47 39
constexpr const char *kSoftware = "SagoImageBrowser";
ac476ca8 Poul Sander 2026-05-18 16:47 40
ac476ca8 Poul Sander 2026-05-18 16:47 41
QFile::Permissions filePerms()
ac476ca8 Poul Sander 2026-05-18 16:47 42
{
ac476ca8 Poul Sander 2026-05-18 16:47 43
    return QFile::ReadOwner | QFile::WriteOwner;
ac476ca8 Poul Sander 2026-05-18 16:47 44
}
ac476ca8 Poul Sander 2026-05-18 16:47 45
ac476ca8 Poul Sander 2026-05-18 16:47 46
QFile::Permissions dirPerms()
ac476ca8 Poul Sander 2026-05-18 16:47 47
{
ac476ca8 Poul Sander 2026-05-18 16:47 48
    return QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner;
ac476ca8 Poul Sander 2026-05-18 16:47 49
}
ac476ca8 Poul Sander 2026-05-18 16:47 50
} // namespace
ac476ca8 Poul Sander 2026-05-18 16:47 51
ac476ca8 Poul Sander 2026-05-18 16:47 52
QString ThumbnailCache::cacheRoot()
ac476ca8 Poul Sander 2026-05-18 16:47 53
{
ac476ca8 Poul Sander 2026-05-18 16:47 54
    QString base = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation);
ac476ca8 Poul Sander 2026-05-18 16:47 55
    if (base.isEmpty())
ac476ca8 Poul Sander 2026-05-18 16:47 56
        base = QDir::homePath() + "/.cache";
ac476ca8 Poul Sander 2026-05-18 16:47 57
    return base + "/thumbnails";
ac476ca8 Poul Sander 2026-05-18 16:47 58
}
ac476ca8 Poul Sander 2026-05-18 16:47 59
ac476ca8 Poul Sander 2026-05-18 16:47 60
QString ThumbnailCache::subdirFor(Size size)
ac476ca8 Poul Sander 2026-05-18 16:47 61
{
ac476ca8 Poul Sander 2026-05-18 16:47 62
    const char *name = (size == Size::Large) ? "large" : "normal";
ac476ca8 Poul Sander 2026-05-18 16:47 63
    return cacheRoot() + "/" + name;
ac476ca8 Poul Sander 2026-05-18 16:47 64
}
ac476ca8 Poul Sander 2026-05-18 16:47 65
80a9926d Poul Sander 2026-05-19 20:28 66
QByteArray ThumbnailCache::canonicalUri(const QByteArray &absPath)
ac476ca8 Poul Sander 2026-05-18 16:47 67
{
80a9926d Poul Sander 2026-05-19 20:28 68
    // Build a correct file:// URI by percent-encoding the raw byte path.
80a9926d Poul Sander 2026-05-19 20:28 69
    // Forward slashes are kept unencoded; all other bytes (including non-UTF-8
80a9926d Poul Sander 2026-05-19 20:28 70
    // ones such as Latin-1 \xF8) are percent-encoded.
80a9926d Poul Sander 2026-05-19 20:28 71
    return "file://" + QUrl::toPercentEncoding(absPath, "/");
ac476ca8 Poul Sander 2026-05-18 16:47 72
}
ac476ca8 Poul Sander 2026-05-18 16:47 73
80a9926d Poul Sander 2026-05-19 20:28 74
QString ThumbnailCache::hashedName(const QByteArray &canonicalUri)
ac476ca8 Poul Sander 2026-05-18 16:47 75
{
80a9926d Poul Sander 2026-05-19 20:28 76
    QByteArray hash =
80a9926d Poul Sander 2026-05-19 20:28 77
        QCryptographicHash::hash(canonicalUri, QCryptographicHash::Md5).toHex();
ac476ca8 Poul Sander 2026-05-18 16:47 78
    return QString::fromLatin1(hash) + ".png";
ac476ca8 Poul Sander 2026-05-18 16:47 79
}
ac476ca8 Poul Sander 2026-05-18 16:47 80
80a9926d Poul Sander 2026-05-19 20:28 81
bool ThumbnailCache::isInsideCache(const QByteArray &absPath)
ac476ca8 Poul Sander 2026-05-18 16:47 82
{
80a9926d Poul Sander 2026-05-19 20:28 83
    QByteArray root = cacheRoot().toUtf8(); // cache root is always valid UTF-8
80a9926d Poul Sander 2026-05-19 20:28 84
    return absPath.startsWith(root + '/') || absPath == root;
ac476ca8 Poul Sander 2026-05-18 16:47 85
}
ac476ca8 Poul Sander 2026-05-18 16:47 86
4d6b7476 Poul Sander 2026-05-30 16:39 87
// THREADING: Called from worker threads (ThumbnailWorker::run()).  This
4d6b7476 Poul Sander 2026-05-30 16:39 88
// function is free of shared mutable state — it only performs filesystem I/O
4d6b7476 Poul Sander 2026-05-30 16:39 89
// using its arguments.  Safe to call concurrently from multiple threads.
80a9926d Poul Sander 2026-05-19 20:28 90
QImage ThumbnailCache::load(const QByteArray &sourcePath, Size size)
ac476ca8 Poul Sander 2026-05-18 16:47 91
{
80a9926d Poul Sander 2026-05-19 20:28 92
    struct ::stat st{};
80a9926d Poul Sander 2026-05-19 20:28 93
    if (::stat(sourcePath.constData(), &st) != 0 || !S_ISREG(st.st_mode))
ac476ca8 Poul Sander 2026-05-18 16:47 94
        return {};
ac476ca8 Poul Sander 2026-05-18 16:47 95
80a9926d Poul Sander 2026-05-19 20:28 96
    const QByteArray uri = canonicalUri(sourcePath);
ac476ca8 Poul Sander 2026-05-18 16:47 97
    const QString cachePath = subdirFor(size) + "/" + hashedName(uri);
ac476ca8 Poul Sander 2026-05-18 16:47 98
ac476ca8 Poul Sander 2026-05-18 16:47 99
    QImage img;
ac476ca8 Poul Sander 2026-05-18 16:47 100
    if (!img.load(cachePath, "PNG"))
ac476ca8 Poul Sander 2026-05-18 16:47 101
        return {};
ac476ca8 Poul Sander 2026-05-18 16:47 102
80a9926d Poul Sander 2026-05-19 20:28 103
    const qint64 mtime = static_cast<qint64>(st.st_mtime);
ac476ca8 Poul Sander 2026-05-18 16:47 104
    bool ok = false;
ac476ca8 Poul Sander 2026-05-18 16:47 105
    const qint64 cachedMTime = img.text("Thumb::MTime").toLongLong(&ok);
ac476ca8 Poul Sander 2026-05-18 16:47 106
    if (!ok || cachedMTime != mtime)
ac476ca8 Poul Sander 2026-05-18 16:47 107
        return {};
ac476ca8 Poul Sander 2026-05-18 16:47 108
ac476ca8 Poul Sander 2026-05-18 16:47 109
    return img;
ac476ca8 Poul Sander 2026-05-18 16:47 110
}
ac476ca8 Poul Sander 2026-05-18 16:47 111
4d6b7476 Poul Sander 2026-05-30 16:39 112
// THREADING: Called from worker threads (ThumbnailWorker::run()).  This
4d6b7476 Poul Sander 2026-05-30 16:39 113
// function is free of shared mutable state — it only performs filesystem I/O
4d6b7476 Poul Sander 2026-05-30 16:39 114
// using its arguments.  Writes are atomic (temp file + rename) so concurrent
4d6b7476 Poul Sander 2026-05-30 16:39 115
// calls for different source files are safe.
80a9926d Poul Sander 2026-05-19 20:28 116
void ThumbnailCache::save(const QByteArray &sourcePath, Size size, const QImage &thumbImage)
ac476ca8 Poul Sander 2026-05-18 16:47 117
{
ac476ca8 Poul Sander 2026-05-18 16:47 118
    if (thumbImage.isNull())
ac476ca8 Poul Sander 2026-05-18 16:47 119
        return;
ac476ca8 Poul Sander 2026-05-18 16:47 120
80a9926d Poul Sander 2026-05-19 20:28 121
    struct ::stat st{};
80a9926d Poul Sander 2026-05-19 20:28 122
    if (::stat(sourcePath.constData(), &st) != 0 || !S_ISREG(st.st_mode))
ac476ca8 Poul Sander 2026-05-18 16:47 123
        return;
ac476ca8 Poul Sander 2026-05-18 16:47 124
80a9926d Poul Sander 2026-05-19 20:28 125
    const QByteArray uri = canonicalUri(sourcePath);
ac476ca8 Poul Sander 2026-05-18 16:47 126
    const QString subdir = subdirFor(size);
ac476ca8 Poul Sander 2026-05-18 16:47 127
    const QString finalPath = subdir + "/" + hashedName(uri);
ac476ca8 Poul Sander 2026-05-18 16:47 128
ac476ca8 Poul Sander 2026-05-18 16:47 129
    QDir dir;
ac476ca8 Poul Sander 2026-05-18 16:47 130
    if (!dir.mkpath(subdir))
ac476ca8 Poul Sander 2026-05-18 16:47 131
        return;
ac476ca8 Poul Sander 2026-05-18 16:47 132
    QFile::setPermissions(subdir, dirPerms());
ac476ca8 Poul Sander 2026-05-18 16:47 133
    QFile::setPermissions(cacheRoot(), dirPerms());
ac476ca8 Poul Sander 2026-05-18 16:47 134
ac476ca8 Poul Sander 2026-05-18 16:47 135
    QImage tagged = thumbImage;
80a9926d Poul Sander 2026-05-19 20:28 136
    tagged.setText("Thumb::URI", QString::fromLatin1(uri));
ac476ca8 Poul Sander 2026-05-18 16:47 137
    tagged.setText("Thumb::MTime",
80a9926d Poul Sander 2026-05-19 20:28 138
                   QString::number(static_cast<qint64>(st.st_mtime)));
80a9926d Poul Sander 2026-05-19 20:28 139
    tagged.setText("Thumb::Size", QString::number(static_cast<qint64>(st.st_size)));
ac476ca8 Poul Sander 2026-05-18 16:47 140
    tagged.setText("Software", QString::fromLatin1(kSoftware));
ac476ca8 Poul Sander 2026-05-18 16:47 141
ac476ca8 Poul Sander 2026-05-18 16:47 142
    QTemporaryFile tmp(subdir + "/tmp_XXXXXX.png");
ac476ca8 Poul Sander 2026-05-18 16:47 143
    tmp.setAutoRemove(false);
ac476ca8 Poul Sander 2026-05-18 16:47 144
    if (!tmp.open())
ac476ca8 Poul Sander 2026-05-18 16:47 145
        return;
ac476ca8 Poul Sander 2026-05-18 16:47 146
    const QString tmpPath = tmp.fileName();
ac476ca8 Poul Sander 2026-05-18 16:47 147
    tmp.close();
ac476ca8 Poul Sander 2026-05-18 16:47 148
ac476ca8 Poul Sander 2026-05-18 16:47 149
    if (!tagged.save(tmpPath, "PNG"))
ac476ca8 Poul Sander 2026-05-18 16:47 150
    {
ac476ca8 Poul Sander 2026-05-18 16:47 151
        QFile::remove(tmpPath);
ac476ca8 Poul Sander 2026-05-18 16:47 152
        return;
ac476ca8 Poul Sander 2026-05-18 16:47 153
    }
ac476ca8 Poul Sander 2026-05-18 16:47 154
ac476ca8 Poul Sander 2026-05-18 16:47 155
    QFile::setPermissions(tmpPath, filePerms());
ac476ca8 Poul Sander 2026-05-18 16:47 156
ac476ca8 Poul Sander 2026-05-18 16:47 157
    // Atomic replace: remove existing target then rename.
ac476ca8 Poul Sander 2026-05-18 16:47 158
    QFile::remove(finalPath);
ac476ca8 Poul Sander 2026-05-18 16:47 159
    if (!QFile::rename(tmpPath, finalPath))
ac476ca8 Poul Sander 2026-05-18 16:47 160
        QFile::remove(tmpPath);
ac476ca8 Poul Sander 2026-05-18 16:47 161
}
1970-01-01 00:00 162