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