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