cutelyst 4.3.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
staticcompressed.cpp
1/*
2 * SPDX-FileCopyrightText: (C) 2017-2023 Matthias Fehring <mf@huessenbergnetz.de>
3 * SPDX-License-Identifier: BSD-3-Clause
4 */
5
6#include "staticcompressed_p.h"
7
8#include <Cutelyst/Application>
9#include <Cutelyst/Context>
10#include <Cutelyst/Engine>
11#include <Cutelyst/Request>
12#include <Cutelyst/Response>
13#include <array>
14#include <chrono>
15
16#include <QCoreApplication>
17#include <QCryptographicHash>
18#include <QDataStream>
19#include <QDateTime>
20#include <QFile>
21#include <QLockFile>
22#include <QLoggingCategory>
23#include <QMimeDatabase>
24#include <QStandardPaths>
25
26#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
27# include <zopfli.h>
28#endif
29
30#ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
31# include <brotli/encode.h>
32#endif
33
34using namespace Cutelyst;
35
36Q_LOGGING_CATEGORY(C_STATICCOMPRESSED, "cutelyst.plugin.staticcompressed", QtWarningMsg)
37
39 : Plugin(parent)
40 , d_ptr(new StaticCompressedPrivate)
41{
43 d->includePaths.append(parent->config(u"root"_qs).toString());
44}
45
47
49{
51 d->includePaths.clear();
52 for (const QString &path : paths) {
53 d->includePaths.append(QDir(path));
54 }
55}
56
58{
60 d->dirs = dirs;
61}
62
64{
66 d->serveDirsOnly = dirsOnly;
67}
68
70{
72
73 const QVariantMap config = app->engine()->config(u"Cutelyst_StaticCompressed_Plugin"_qs);
74 const QString _defaultCacheDir =
76 QLatin1String("/compressed-static");
77 d->cacheDir.setPath(config
78 .value(u"cache_directory"_qs,
79 d->defaultConfig.value(u"cache_directory"_qs, _defaultCacheDir))
80 .toString());
81
82 if (Q_UNLIKELY(!d->cacheDir.exists())) {
83 if (!d->cacheDir.mkpath(d->cacheDir.absolutePath())) {
84 qCCritical(C_STATICCOMPRESSED)
85 << "Failed to create cache directory for compressed static files at"
86 << d->cacheDir.absolutePath();
87 return false;
88 }
89 }
90
91 qCInfo(C_STATICCOMPRESSED) << "Compressed cache directory:" << d->cacheDir.absolutePath();
92
93 const QString _mimeTypes =
94 config
95 .value(u"mime_types"_qs,
96 d->defaultConfig.value(u"mime_types"_qs,
97 u"text/css,application/javascript,text/javascript"_qs))
98 .toString();
99 qCInfo(C_STATICCOMPRESSED) << "MIME Types:" << _mimeTypes;
100 d->mimeTypes = _mimeTypes.split(u',', Qt::SkipEmptyParts);
101
102 const QString _suffixes =
103 config
104 .value(
105 u"suffixes"_qs,
106 d->defaultConfig.value(u"suffixes"_qs, u"js.map,css.map,min.js.map,min.css.map"_qs))
107 .toString();
108 qCInfo(C_STATICCOMPRESSED) << "Suffixes:" << _suffixes;
109 d->suffixes = _suffixes.split(u',', Qt::SkipEmptyParts);
110
111 d->checkPreCompressed = config
112 .value(u"check_pre_compressed"_qs,
113 d->defaultConfig.value(u"check_pre_compressed"_qs, true))
114 .toBool();
115 qCInfo(C_STATICCOMPRESSED) << "Check for pre-compressed files:" << d->checkPreCompressed;
116
117 d->onTheFlyCompression = config
118 .value(u"on_the_fly_compression"_qs,
119 d->defaultConfig.value(u"on_the_fly_compression"_qs, true))
120 .toBool();
121 qCInfo(C_STATICCOMPRESSED) << "Compress static files on the fly:" << d->onTheFlyCompression;
122
123 QStringList supportedCompressions{u"deflate"_qs, u"gzip"_qs};
124
125 bool ok = false;
126 d->zlibCompressionLevel =
127 config
128 .value(u"zlib_compression_level"_qs,
129 d->defaultConfig.value(u"zlib_compression_level"_qs,
130 StaticCompressedPrivate::zlibCompressionLevelDefault))
131 .toInt(&ok);
132 if (!ok) {
133 qCWarning(C_STATICCOMPRESSED).nospace()
134 << "Invalid value set for zlib_compression_level. "
135 "Has to to be an integer value between "
136 << StaticCompressedPrivate::zlibCompressionLevelMin << " and "
137 << StaticCompressedPrivate::zlibCompressionLevelMax
138 << " inclusive. Using default value "
139 << StaticCompressedPrivate::zlibCompressionLevelDefault;
140 }
141
142 if (d->zlibCompressionLevel < StaticCompressedPrivate::zlibCompressionLevelMin ||
143 d->zlibCompressionLevel > StaticCompressedPrivate::zlibCompressionLevelMax) {
144 qCWarning(C_STATICCOMPRESSED).nospace()
145 << "Invalid value " << d->zlibCompressionLevel
146 << " set for zlib_compression_level. Value hat to be between "
147 << StaticCompressedPrivate::zlibCompressionLevelMin << " and "
148 << StaticCompressedPrivate::zlibCompressionLevelMax
149 << " inclusive. Using default value "
150 << StaticCompressedPrivate::zlibCompressionLevelDefault;
151 d->zlibCompressionLevel = StaticCompressedPrivate::zlibCompressionLevelDefault;
152 }
153
154#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
155 supportedCompressions << u"zopfli"_qs;
156 d->useZopfli =
157 config.value(u"use_zopfli"_qs, d->defaultConfig.value(u"use_zopfli"_qs, false)).toBool();
158 if (d->useZopfli) {
159 d->zopfliIterations =
160 config
161 .value(u"zopfli_iterations"_qs,
162 d->defaultConfig.value(u"zopfli_iterations"_qs,
163 StaticCompressedPrivate::zopfliIterationsDefault))
164 .toInt(&ok);
165 if (!ok) {
166 qCWarning(C_STATICCOMPRESSED).nospace()
167 << "Invalid value for zopfli_iterations. "
168 "Has to be an integer value greater than or equal to "
169 << StaticCompressedPrivate::zopfliIterationsMin << ". Using default value "
170 << StaticCompressedPrivate::zopfliIterationsDefault;
171 d->zopfliIterations = StaticCompressedPrivate::zopfliIterationsDefault;
172 }
173
174 if (d->zopfliIterations < StaticCompressedPrivate::zopfliIterationsMin) {
175 qCWarning(C_STATICCOMPRESSED).nospace()
176 << "Invalid value " << d->zopfliIterations
177 << " set for zopfli_iterations. Value has to to be greater than or equal to "
178 << StaticCompressedPrivate::zopfliIterationsMin << ". Using default value "
179 << StaticCompressedPrivate::zopfliIterationsDefault;
180 d->zopfliIterations = StaticCompressedPrivate::zopfliIterationsDefault;
181 }
182 }
183#endif
184
185#ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
186 d->brotliQualityLevel =
187 config
188 .value(u"brotli_quality_level"_qs,
189 d->defaultConfig.value(u"brotli_quality_level"_qs,
190 StaticCompressedPrivate::brotliQualityLevelDefault))
191 .toInt(&ok);
192 if (!ok) {
193 qCWarning(C_STATICCOMPRESSED).nospace()
194 << "Invalid value for brotli_quality_level. "
195 "Has to be an integer value between "
196 << BROTLI_MIN_QUALITY << " and " << BROTLI_MAX_QUALITY
197 << " inclusive. Using default value "
198 << StaticCompressedPrivate::brotliQualityLevelDefault;
199 d->brotliQualityLevel = StaticCompressedPrivate::brotliQualityLevelDefault;
200 }
201
202 if (d->brotliQualityLevel < BROTLI_MIN_QUALITY || d->brotliQualityLevel > BROTLI_MAX_QUALITY) {
203 qCWarning(C_STATICCOMPRESSED).nospace()
204 << "Invalid value " << d->brotliQualityLevel
205 << " set for brotli_quality_level. Value has to be between " << BROTLI_MIN_QUALITY
206 << " and " << BROTLI_MAX_QUALITY << " inclusive. Using default value "
207 << StaticCompressedPrivate::brotliQualityLevelDefault;
208 d->brotliQualityLevel = StaticCompressedPrivate::brotliQualityLevelDefault;
209 }
210 supportedCompressions << u"brotli"_qs;
211#endif
212
213 qCInfo(C_STATICCOMPRESSED) << "Supported compressions:" << supportedCompressions.join(u',');
214 qCInfo(C_STATICCOMPRESSED) << "Include paths:" << d->includePaths;
215
216 connect(app, &Application::beforePrepareAction, this, [d](Context *c, bool *skipMethod) {
217 d->beforePrepareAction(c, skipMethod);
218 });
219
220 return true;
221}
222
223void StaticCompressedPrivate::beforePrepareAction(Context *c, bool *skipMethod)
224{
225 if (*skipMethod) {
226 return;
227 }
228
229 // TODO mid(1) quick fix for path now having leading slash
230 const QString path = c->req()->path().mid(1);
231
232 for (const QString &dir : dirs) {
233 if (path.startsWith(dir)) {
234 if (!locateCompressedFile(c, path)) {
235 Response *res = c->response();
236 res->setStatus(Response::NotFound);
237 res->setContentType("text/html"_qba);
238 res->setBody(u"File not found: "_qs + path);
239 }
240
241 *skipMethod = true;
242 return;
243 }
244 }
245
246 if (serveDirsOnly) {
247 return;
248 }
249
250 const QRegularExpression _re = re; // Thread-safe
251 const QRegularExpressionMatch match = _re.match(path);
252 if (match.hasMatch() && locateCompressedFile(c, path)) {
253 *skipMethod = true;
254 }
255}
256
257bool StaticCompressedPrivate::locateCompressedFile(Context *c, const QString &relPath) const
258{
259 for (const QDir &includePath : includePaths) {
260 qCDebug(C_STATICCOMPRESSED)
261 << "Trying to find" << relPath << "in" << includePath.absolutePath();
262 const QString path = includePath.absoluteFilePath(relPath);
263 const QFileInfo fileInfo(path);
264 if (fileInfo.exists()) {
265 Response *res = c->res();
266 const QDateTime currentDateTime = fileInfo.lastModified();
267 if (!c->req()->headers().ifModifiedSince(currentDateTime)) {
268 res->setStatus(Response::NotModified);
269 return true;
270 }
271
272 static QMimeDatabase db;
273 // use the extension to match to be faster
275 QByteArray contentEncoding;
276 QString compressedPath;
277 QByteArray _mimeTypeName;
278
279 if (mimeType.isValid()) {
280
281 // QMimeDatabase might not find the correct mime type for some specific types
282 // especially for map files for CSS and JS
283 if (mimeType.isDefault()) {
284 if (path.endsWith(u"css.map", Qt::CaseInsensitive) ||
285 path.endsWith(u"js.map", Qt::CaseInsensitive)) {
286 _mimeTypeName = "application/json"_qba;
287 }
288 }
289
290 if (mimeTypes.contains(mimeType.name(), Qt::CaseInsensitive) ||
291 suffixes.contains(fileInfo.completeSuffix(), Qt::CaseInsensitive)) {
292
293 const auto acceptEncoding = c->req()->header("Accept-Encoding");
294
295#ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
296 if (acceptEncoding.contains("br")) {
297 compressedPath = locateCacheFile(path, currentDateTime, Brotli);
298 if (!compressedPath.isEmpty()) {
299 qCDebug(C_STATICCOMPRESSED)
300 << "Serving brotli compressed data from" << compressedPath;
301 contentEncoding = "br"_qba;
302 }
303 } else
304#endif
305 if (acceptEncoding.contains("gzip")) {
306 compressedPath =
307 locateCacheFile(path, currentDateTime, useZopfli ? Zopfli : Gzip);
308 if (!compressedPath.isEmpty()) {
309 qCDebug(C_STATICCOMPRESSED)
310 << "Serving" << (useZopfli ? "zopfli" : "gzip")
311 << "compressed data from" << compressedPath;
312 contentEncoding = "gzip"_qba;
313 }
314 } else if (acceptEncoding.contains("deflate")) {
315 compressedPath = locateCacheFile(path, currentDateTime, Deflate);
316 if (!compressedPath.isEmpty()) {
317 qCDebug(C_STATICCOMPRESSED)
318 << "Serving deflate compressed data from" << compressedPath;
319 contentEncoding = "deflate"_qba;
320 }
321 }
322 }
323 }
324
325 // Response::setBody() will take the ownership
326 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
327 QFile *file = !compressedPath.isEmpty() ? new QFile(compressedPath) : new QFile(path);
328 if (file->open(QFile::ReadOnly)) {
329 qCDebug(C_STATICCOMPRESSED) << "Serving" << path;
330 Headers &headers = res->headers();
331
332 // set our open file
333 res->setBody(file);
334
335 // if we have a mime type determine from the extension,
336 // do not use the name from the mime database
337 if (!_mimeTypeName.isEmpty()) {
338 headers.setContentType(_mimeTypeName);
339 } else if (mimeType.isValid()) {
340 headers.setContentType(mimeType.name().toLatin1());
341 }
342 headers.setContentLength(file->size());
343
344 headers.setLastModified(currentDateTime);
345 // Tell Firefox & friends its OK to cache, even over SSL
346 headers.setCacheControl("public"_qba);
347
348 if (!contentEncoding.isEmpty()) {
349 // serve correct encoding type
350 headers.setContentEncoding(contentEncoding);
351
352 qCDebug(C_STATICCOMPRESSED)
353 << "Encoding:" << headers.contentEncoding() << "Size:" << file->size()
354 << "Original Size:" << fileInfo.size();
355
356 // force proxies to cache compressed and non-compressed files separately
357 headers.pushHeader("Vary"_qba, "Accept-Encoding"_qba);
358 }
359
360 return true;
361 }
362
363 qCWarning(C_STATICCOMPRESSED) << "Could not serve" << path << file->errorString();
364 delete file;
365 return false;
366 }
367 }
368
369 qCWarning(C_STATICCOMPRESSED) << "File not found" << relPath;
370 return false;
371}
372
373QString StaticCompressedPrivate::locateCacheFile(const QString &origPath,
374 const QDateTime &origLastModified,
375 Compression compression) const
376{
377 QString compressedPath;
378
379 QString suffix;
380
381 switch (compression) {
382 case Zopfli:
383 case Gzip:
384 suffix = u".gz"_qs;
385 break;
386#ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
387 case Brotli:
388 suffix = u".br"_qs;
389 break;
390#endif
391 case Deflate:
392 suffix = u".deflate"_qs;
393 break;
394 default:
395 Q_ASSERT_X(false, "locate cache file", "invalid compression type");
396 break;
397 }
398
399 if (checkPreCompressed) {
400 const QFileInfo origCompressed(origPath + suffix);
401 if (origCompressed.exists()) {
402 compressedPath = origCompressed.absoluteFilePath();
403 return compressedPath;
404 }
405 }
406
407 if (onTheFlyCompression) {
408
409 const QString path = cacheDir.absoluteFilePath(
412 suffix);
413 const QFileInfo info(path);
414
415 if (info.exists() && (info.lastModified() > origLastModified)) {
416 compressedPath = path;
417 } else {
418 QLockFile lock(path + QLatin1String(".lock"));
419 if (lock.tryLock(std::chrono::milliseconds{10})) {
420 switch (compression) {
421#ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
422 case Brotli:
423 if (compressBrotli(origPath, path)) {
424 compressedPath = path;
425 }
426 break;
427#endif
428 case Zopfli:
429#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
430 if (compressZopfli(origPath, path)) {
431 compressedPath = path;
432 }
433 break;
434#endif
435 case Gzip:
436 if (compressGzip(origPath, path, origLastModified)) {
437 compressedPath = path;
438 }
439 break;
440 case Deflate:
441 if (compressDeflate(origPath, path)) {
442 compressedPath = path;
443 }
444 break;
445 default:
446 break;
447 }
448 lock.unlock();
449 }
450 }
451 }
452
453 return compressedPath;
454}
455
456// clang-format off
457static constexpr std::array<quint32, 256> crc_32_tab { /* CRC polynomial 0xedb88320 */
458 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
459 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
460 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
461 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
462 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
463 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
464 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
465 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
466 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
467 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
468 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
469 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
470 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
471 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
472 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
473 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
474 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
475 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
476 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
477 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
478 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
479 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
480 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
481 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
482 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
483 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
484 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
485 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
486 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
487 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
488 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
489 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
490 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
491 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
492 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
493 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
494 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
495 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
496 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
497 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
498 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
499 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
500 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
501};
502// clang-format on
503
504quint32 updateCRC32(unsigned char ch, quint32 crc)
505{
506 // NOLINTNEXTLINE(cppcoreguidelines-avoid-magic-numbers)
507 return (crc_32_tab[((crc) ^ (quint8(ch))) & 0xff] ^ ((crc) >> 8));
508}
509
510quint32 crc32buf(const QByteArray &data)
511{
512 return ~std::accumulate(data.begin(),
513 data.end(),
514 quint32(0xFFFFFFFF), // NOLINT(cppcoreguidelines-avoid-magic-numbers)
515 [](quint32 oldcrc32, char buf) { return updateCRC32(buf, oldcrc32); });
516}
517
518bool StaticCompressedPrivate::compressGzip(const QString &inputPath,
519 const QString &outputPath,
520 const QDateTime &origLastModified) const
521{
522 qCDebug(C_STATICCOMPRESSED) << "Compressing" << inputPath << "with gzip to" << outputPath;
523
524 QFile input(inputPath);
525 if (Q_UNLIKELY(!input.open(QIODevice::ReadOnly))) {
526 qCWarning(C_STATICCOMPRESSED)
527 << "Can not open input file to compress with gzip:" << inputPath;
528 return false;
529 }
530
531 const QByteArray data = input.readAll();
532 if (Q_UNLIKELY(data.isEmpty())) {
533 qCWarning(C_STATICCOMPRESSED)
534 << "Can not read input file or input file is empty:" << inputPath;
535 input.close();
536 return false;
537 }
538
539 QByteArray compressedData = qCompress(data, zlibCompressionLevel);
540 input.close();
541
542 QFile output(outputPath);
543 if (Q_UNLIKELY(!output.open(QIODevice::WriteOnly))) {
544 qCWarning(C_STATICCOMPRESSED)
545 << "Can not open output file to compress with gzip:" << outputPath;
546 return false;
547 }
548
549 if (Q_UNLIKELY(compressedData.isEmpty())) {
550 qCWarning(C_STATICCOMPRESSED)
551 << "Failed to compress file with gzip, compressed data is empty:" << inputPath;
552 if (output.exists()) {
553 if (Q_UNLIKELY(!output.remove())) {
554 qCWarning(C_STATICCOMPRESSED)
555 << "Can not remove invalid compressed gzip file:" << outputPath;
556 }
557 }
558 return false;
559 }
560
561 // Strip the first six bytes (a 4-byte length put on by qCompress and a 2-byte zlib header)
562 // and the last four bytes (a zlib integrity check).
563 compressedData.remove(0, 6);
564 compressedData.chop(4);
565
566 QByteArray header;
567 QDataStream headerStream(&header, QIODevice::WriteOnly);
568 // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers)
569 // prepend a generic 10-byte gzip header (see RFC 1952)
570 headerStream << quint16(0x1f8b) << quint16(0x0800)
571 << quint32(origLastModified.toSecsSinceEpoch())
572#if defined Q_OS_UNIX
573 << quint16(0x0003);
574#elif defined Q_OS_WIN
575 << quint16(0x000b);
576#elif defined Q_OS_MACOS
577 << quint16(0x0007);
578#else
579 << quint16(0x00ff);
580#endif
581 // NOLINTEND(cppcoreguidelines-avoid-magic-numbers)
582
583 // append a four-byte CRC-32 of the uncompressed data
584 // append 4 bytes uncompressed input size modulo 2^32
585 QByteArray footer;
586 QDataStream footerStream(&footer, QIODevice::WriteOnly);
587 footerStream << crc32buf(data) << quint32(data.size());
588
589 if (Q_UNLIKELY(output.write(header + compressedData + footer) < 0)) {
590 qCCritical(C_STATICCOMPRESSED).nospace()
591 << "Failed to write compressed gzip file " << inputPath << ": " << output.errorString();
592 return false;
593 }
594
595 return true;
596}
597
598bool StaticCompressedPrivate::compressDeflate(const QString &inputPath,
599 const QString &outputPath) const
600{
601 qCDebug(C_STATICCOMPRESSED) << "Compressing" << inputPath << "with deflate to" << outputPath;
602
603 QFile input(inputPath);
604 if (Q_UNLIKELY(!input.open(QIODevice::ReadOnly))) {
605 qCWarning(C_STATICCOMPRESSED)
606 << "Can not open input file to compress with deflate:" << inputPath;
607 return false;
608 }
609
610 const QByteArray data = input.readAll();
611 if (Q_UNLIKELY(data.isEmpty())) {
612 qCWarning(C_STATICCOMPRESSED)
613 << "Can not read input file or input file is empty:" << inputPath;
614 input.close();
615 return false;
616 }
617
618 QByteArray compressedData = qCompress(data, zlibCompressionLevel);
619 input.close();
620
621 QFile output(outputPath);
622 if (Q_UNLIKELY(!output.open(QIODevice::WriteOnly))) {
623 qCWarning(C_STATICCOMPRESSED)
624 << "Can not open output file to compress with deflate:" << outputPath;
625 return false;
626 }
627
628 if (Q_UNLIKELY(compressedData.isEmpty())) {
629 qCWarning(C_STATICCOMPRESSED)
630 << "Failed to compress file with deflate, compressed data is empty:" << inputPath;
631 if (output.exists()) {
632 if (Q_UNLIKELY(!output.remove())) {
633 qCWarning(C_STATICCOMPRESSED)
634 << "Can not remove invalid compressed deflate file:" << outputPath;
635 }
636 }
637 return false;
638 }
639
640 // Strip the first six bytes (a 4-byte length put on by qCompress and a 2-byte zlib header)
641 // and the last four bytes (a zlib integrity check).
642 compressedData.remove(0, 6);
643 compressedData.chop(4);
644
645 if (Q_UNLIKELY(output.write(compressedData) < 0)) {
646 qCCritical(C_STATICCOMPRESSED).nospace() << "Failed to write compressed deflate file "
647 << inputPath << ": " << output.errorString();
648 return false;
649 }
650
651 return true;
652}
653
654#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
655bool StaticCompressedPrivate::compressZopfli(const QString &inputPath,
656 const QString &outputPath) const
657{
658 qCDebug(C_STATICCOMPRESSED) << "Compressing" << inputPath << "with zopfli to" << outputPath;
659
660 QFile input(inputPath);
661 if (Q_UNLIKELY(!input.open(QIODevice::ReadOnly))) {
662 qCWarning(C_STATICCOMPRESSED)
663 << "Can not open input file to compress with zopfli:" << inputPath;
664 return false;
665 }
666
667 const QByteArray data = input.readAll();
668 if (Q_UNLIKELY(data.isEmpty())) {
669 qCWarning(C_STATICCOMPRESSED)
670 << "Can not read input file or input file is empty:" << inputPath;
671 input.close();
672 return false;
673 }
674
675 ZopfliOptions options;
676 ZopfliInitOptions(&options);
677 options.numiterations = zopfliIterations;
678
679 unsigned char *out{nullptr};
680 size_t outSize{0};
681
682 ZopfliCompress(&options,
683 ZopfliFormat::ZOPFLI_FORMAT_GZIP,
684 reinterpret_cast<const unsigned char *>(data.constData()),
685 data.size(),
686 &out,
687 &outSize);
688
689 bool ok = false;
690 if (outSize > 0) {
691 QFile output(outputPath);
692 if (Q_UNLIKELY(!output.open(QIODevice::WriteOnly))) {
693 qCWarning(C_STATICCOMPRESSED)
694 << "Can not open output file to compress with zopfli:" << outputPath;
695 } else {
696 if (Q_UNLIKELY(output.write(reinterpret_cast<const char *>(out), outSize) < 0)) {
697 qCCritical(C_STATICCOMPRESSED).nospace()
698 << "Failed to write compressed zopfi file " << inputPath << ": "
699 << output.errorString();
700 if (output.exists()) {
701 if (Q_UNLIKELY(!output.remove())) {
702 qCWarning(C_STATICCOMPRESSED)
703 << "Can not remove invalid compressed zopfli file:" << outputPath;
704 }
705 }
706 } else {
707 ok = true;
708 }
709 }
710 } else {
711 qCWarning(C_STATICCOMPRESSED)
712 << "Failed to compress file with zopfli, compressed data is empty:" << inputPath;
713 }
714
715 free(out);
716
717 return ok;
718}
719#endif
720
721#ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
722bool StaticCompressedPrivate::compressBrotli(const QString &inputPath,
723 const QString &outputPath) const
724{
725 qCDebug(C_STATICCOMPRESSED) << "Compressing" << inputPath << "with brotli to" << outputPath;
726
727 QFile input(inputPath);
728 if (Q_UNLIKELY(!input.open(QIODevice::ReadOnly))) {
729 qCWarning(C_STATICCOMPRESSED)
730 << "Can not open input file to compress with brotli:" << inputPath;
731 return false;
732 }
733
734 const QByteArray data = input.readAll();
735 if (Q_UNLIKELY(data.isEmpty())) {
736 qCWarning(C_STATICCOMPRESSED)
737 << "Can not read input file or input file is empty:" << inputPath;
738 return false;
739 }
740
741 input.close();
742
743 bool ok = false;
744
745 size_t outSize = BrotliEncoderMaxCompressedSize(static_cast<size_t>(data.size()));
746 if (Q_LIKELY(outSize > 0)) {
747 const auto in = reinterpret_cast<const uint8_t *>(data.constData());
748 auto out = static_cast<uint8_t *>(malloc(sizeof(uint8_t) * (outSize + 1)));
749 if (Q_LIKELY(out != nullptr)) {
750 BROTLI_BOOL status = BrotliEncoderCompress(brotliQualityLevel,
751 BROTLI_DEFAULT_WINDOW,
752 BROTLI_DEFAULT_MODE,
753 data.size(),
754 in,
755 &outSize,
756 out);
757 if (Q_LIKELY(status == BROTLI_TRUE)) {
758 QFile output(outputPath);
759 if (Q_LIKELY(output.open(QIODevice::WriteOnly))) {
760 if (Q_LIKELY(output.write(reinterpret_cast<const char *>(out), outSize) > -1)) {
761 ok = true;
762 } else {
763 qCWarning(C_STATICCOMPRESSED).nospace()
764 << "Failed to write brotli compressed data to output file "
765 << outputPath << ": " << output.errorString();
766 if (output.exists()) {
767 if (Q_UNLIKELY(!output.remove())) {
768 qCWarning(C_STATICCOMPRESSED)
769 << "Can not remove invalid compressed brotli file:"
770 << outputPath;
771 }
772 }
773 }
774 } else {
775 qCWarning(C_STATICCOMPRESSED)
776 << "Failed to open output file for brotli compression:" << outputPath;
777 }
778 } else {
779 qCWarning(C_STATICCOMPRESSED) << "Failed to compress" << inputPath << "with brotli";
780 }
781 free(out);
782 } else {
783 qCWarning(C_STATICCOMPRESSED)
784 << "Can not allocate needed output buffer of size"
785 << (sizeof(uint8_t) * (outSize + 1)) << "for brotli compression.";
786 }
787 } else {
788 qCWarning(C_STATICCOMPRESSED) << "Needed output buffer too large to compress input of size"
789 << data.size() << "with brotli";
790 }
791
792 return ok;
793}
794#endif
795
796#include "moc_staticcompressed.cpp"
The Cutelyst application.
Definition application.h:66
Engine * engine() const noexcept
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
The Cutelyst Context.
Definition context.h:42
Response * res() const noexcept
Definition context.cpp:103
Request * req
Definition context.h:66
Response * response() const noexcept
Definition context.cpp:97
QVariantMap config(const QString &entity) const
Definition engine.cpp:263
Container for HTTP headers.
Definition headers.h:24
QByteArray contentEncoding() const noexcept
Definition headers.cpp:57
QByteArray ifModifiedSince() const noexcept
Definition headers.cpp:205
void setContentLength(qint64 value)
Definition headers.cpp:172
void setLastModified(const QByteArray &value)
Definition headers.cpp:271
void setCacheControl(const QByteArray &value)
Definition headers.cpp:38
void pushHeader(const QByteArray &key, const QByteArray &value)
Definition headers.cpp:458
void setContentType(const QByteArray &contentType)
Definition headers.cpp:76
void setContentEncoding(const QByteArray &encoding)
Definition headers.cpp:62
Base class for Cutelyst Plugins.
Definition plugin.h:25
Headers headers() const noexcept
Definition request.cpp:312
QByteArray header(QByteArrayView key) const noexcept
Definition request.h:611
A Cutelyst response.
Definition response.h:29
void setContentType(const QByteArray &type)
Definition response.h:238
void setStatus(quint16 status) noexcept
Definition response.cpp:72
void setBody(QIODevice *body)
Definition response.cpp:103
Headers & headers() noexcept
Serve static files compressed on the fly or pre-compressed.
void setServeDirsOnly(bool dirsOnly)
void setIncludePaths(const QStringList &paths)
void setDirs(const QStringList &dirs)
bool setup(Application *app) override
The Cutelyst namespace holds all public Cutelyst API.
QByteArray::iterator begin()
void chop(qsizetype n)
const char * constData() const const
QByteArray::iterator end()
bool isEmpty() const const
QByteArray & remove(qsizetype pos, qsizetype len)
qsizetype size() const const
QByteArray toHex(char separator) const const
QByteArray hash(QByteArrayView data, QCryptographicHash::Algorithm method)
qint64 toSecsSinceEpoch() const const
bool open(FILE *fh, QIODeviceBase::OpenMode mode, QFileDevice::FileHandleFlags handleFlags)
virtual qint64 size() const const override
QString errorString() const const
T value(qsizetype i) const const
QMimeType mimeTypeForFile(const QFileInfo &fileInfo, QMimeDatabase::MatchMode mode) const const
bool isValid() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QRegularExpressionMatch match(QStringView subjectView, qsizetype offset, QRegularExpression::MatchType matchType, QRegularExpression::MatchOptions matchOptions) const const
bool hasMatch() const const
QString writableLocation(QStandardPaths::StandardLocation type)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
QString mid(qsizetype position, qsizetype n) const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toUtf8() const const
CaseInsensitive
SkipEmptyParts