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