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