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