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