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