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 
32 using namespace Cutelyst;
33 
34 Q_LOGGING_CATEGORY(C_STATICCOMPRESSED, "cutelyst.plugin.staticcompressed", QtWarningMsg)
35 
37  : Plugin(parent)
38  , d_ptr(new StaticCompressedPrivate)
39 {
40  Q_D(StaticCompressed);
41  d->includePaths.append(parent->config(QStringLiteral("root")).toString());
42 }
43 
45 {
46 }
47 
49 {
50  Q_D(StaticCompressed);
51  d->includePaths.clear();
52  for (const QString &path : paths) {
53  d->includePaths.append(QDir(path));
54  }
55 }
56 
58 {
59  Q_D(StaticCompressed);
60  d->dirs = dirs;
61 }
62 
64 {
65  Q_D(StaticCompressed);
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 
150 void 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 
179 bool 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 
292 QString 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
376 static 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 
423 quint32 updateCRC32(unsigned char ch, quint32 crc)
424 {
425  return (crc_32_tab[((crc) ^ (quint8(ch))) & 0xff] ^ ((crc) >> 8));
426 }
427 
428 quint32 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 
436 bool 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 
519 bool 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
581 bool 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
652 bool 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"
void pushHeader(const QString &field, const QString &value)
Definition: headers.cpp:406
virtual bool setup(Application *app) override
QRegularExpressionMatch match(const QString &subject, int offset, QRegularExpression::MatchType matchType, QRegularExpression::MatchOptions matchOptions) const const
void setContentType(const QString &type)
Definition: response.h:220
QString writableLocation(QStandardPaths::StandardLocation type)
Deliver static files compressed on the fly or precompressed.
Headers & headers() noexcept
QString errorString() const const
Response * res() const noexcept
Definition: context.cpp:102
void chop(int n)
bool isEmpty() const const
virtual ~StaticCompressed() override
STL namespace.
T value(int i) const const
QMimeType mimeTypeForFile(const QString &fileName, QMimeDatabase::MatchMode mode) const const
The Cutelyst Context.
Definition: context.h:38
void setDirs(const QStringList &dirs)
Headers headers() const noexcept
Definition: request.cpp:310
CaseInsensitive
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
QVariantMap config(const QString &entity) const
user configuration for the application
Definition: engine.cpp:290
bool isEmpty() const const
const char * constData() const const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString header(const QString &key) const
Definition: request.h:581
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
QByteArray::iterator begin()
bool hasMatch() const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
void setHeader(const QString &field, const QString &value)
Definition: headers.cpp:396
void setLastModified(const QString &value)
Definition: headers.cpp:267
The Cutelyst namespace holds all public Cutelyst API.
Definition: Mainpage.dox:7
virtual bool open(QIODevice::OpenMode mode) override
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
SkipEmptyParts
virtual qint64 size() const const override
bool isValid() const const
void setContentType(const QString &contentType)
Definition: headers.cpp:69
qint64 toSecsSinceEpoch() const const
void setContentEncoding(const QString &encoding)
Definition: headers.cpp:53
QByteArray hash(const QByteArray &data, QCryptographicHash::Algorithm method)
QString fromLatin1(const char *str, int size)
The Cutelyst Application.
Definition: application.h:42
void setIncludePaths(const QStringList &paths)
Engine * engine() const noexcept
QString ifModifiedSince() const
Definition: headers.cpp:200
void setBody(QIODevice *body)
Definition: response.cpp:100
void setContentLength(qint64 value)
Definition: headers.cpp:167
int size() const const
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
Response * response() const noexcept
Definition: context.cpp:96
QByteArray & remove(int pos, int len)
void setStatus(quint16 status) noexcept
Definition: response.cpp:70
QByteArray::iterator end()
QByteArray toUtf8() const const