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