| 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 |
|