6#include "staticcompressed_p.h"
8#include <Cutelyst/Application>
9#include <Cutelyst/Context>
10#include <Cutelyst/Engine>
11#include <Cutelyst/Request>
12#include <Cutelyst/Response>
16#include <QCoreApplication>
17#include <QCryptographicHash>
22#include <QLoggingCategory>
23#include <QMimeDatabase>
24#include <QStandardPaths>
26#ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
27# include <brotli/encode.h>
32Q_LOGGING_CATEGORY(C_STATICCOMPRESSED,
"cutelyst.plugin.staticcompressed", QtWarningMsg)
36 , d_ptr(new StaticCompressedPrivate)
39 d->includePaths.append(parent->config(u
"root"_qs).toString());
44 , d_ptr(new StaticCompressedPrivate)
47 d->includePaths.append(parent->
config(u
"root"_qs).toString());
48 d->defaultConfig = defaultConfig;
56 d->includePaths.clear();
57 for (
const QString &path : paths) {
58 d->includePaths.append(QDir(path));
71 d->serveDirsOnly = dirsOnly;
78 const QVariantMap config = app->
engine()->
config(u
"Cutelyst_StaticCompressed_Plugin"_qs);
79 const QString _defaultCacheDir =
80 QStandardPaths::writableLocation(QStandardPaths::CacheLocation) +
81 QLatin1String(
"/compressed-static");
82 d->cacheDir.setPath(config
83 .value(u
"cache_directory"_qs,
84 d->defaultConfig.value(u
"cache_directory"_qs, _defaultCacheDir))
87 if (Q_UNLIKELY(!d->cacheDir.exists())) {
88 if (!d->cacheDir.mkpath(d->cacheDir.absolutePath())) {
89 qCCritical(C_STATICCOMPRESSED)
90 <<
"Failed to create cache directory for compressed static files at"
91 << d->cacheDir.absolutePath();
96 qCInfo(C_STATICCOMPRESSED) <<
"Compressed cache directory:" << d->cacheDir.absolutePath();
98 const QString _mimeTypes =
100 .value(u
"mime_types"_qs,
101 d->defaultConfig.value(u
"mime_types"_qs,
102 u
"text/css,application/javascript,text/javascript"_qs))
104 qCInfo(C_STATICCOMPRESSED) <<
"MIME Types:" << _mimeTypes;
105 d->mimeTypes = _mimeTypes.split(u
',', Qt::SkipEmptyParts);
107 const QString _suffixes =
111 d->defaultConfig.value(u
"suffixes"_qs, u
"js.map,css.map,min.js.map,min.css.map"_qs))
113 qCInfo(C_STATICCOMPRESSED) <<
"Suffixes:" << _suffixes;
114 d->suffixes = _suffixes.split(u
',', Qt::SkipEmptyParts);
116 d->checkPreCompressed = config
117 .value(u
"check_pre_compressed"_qs,
118 d->defaultConfig.value(u
"check_pre_compressed"_qs,
true))
120 qCInfo(C_STATICCOMPRESSED) <<
"Check for pre-compressed files:" << d->checkPreCompressed;
122 d->onTheFlyCompression = config
123 .value(u
"on_the_fly_compression"_qs,
124 d->defaultConfig.value(u
"on_the_fly_compression"_qs,
true))
126 qCInfo(C_STATICCOMPRESSED) <<
"Compress static files on the fly:" << d->onTheFlyCompression;
128 QStringList supportedCompressions{u
"deflate"_qs, u
"gzip"_qs};
129 d->loadZlibConfig(config);
131#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
132 d->loadZopfliConfig(config);
133 qCInfo(C_STATICCOMPRESSED) <<
"Use Zopfli:" << d->useZopfli;
136#ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
137 d->loadBrotliConfig(config);
138 supportedCompressions << u
"br"_qs;
141#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZSTD
142 if (Q_UNLIKELY(!d->loadZstdConfig(config))) {
145 supportedCompressions << u
"zstd"_qs;
148 const QStringList defaultCompressionFormatOrder{
149#ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
152#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZSTD
158 QStringList _compressionFormatOrder =
160 .value(u
"compression_format_order"_qs,
161 d->defaultConfig.value(u
"compression_format_order"_qs,
162 defaultCompressionFormatOrder.join(u
',')))
164 .split(u
',', Qt::SkipEmptyParts);
165 if (Q_UNLIKELY(_compressionFormatOrder.empty())) {
166 _compressionFormatOrder = defaultCompressionFormatOrder;
167 qCWarning(C_STATICCOMPRESSED)
168 <<
"Invalid or empty value for compression_format_order. Has to be a string list "
169 "containing supported values. Using default value"
170 << defaultCompressionFormatOrder.join(u
',');
172 for (
const auto &cfo : std::as_const(_compressionFormatOrder)) {
173 const QString order = cfo.trimmed().toLower();
174 if (supportedCompressions.contains(order)) {
175 d->compressionFormatOrder << order;
178 if (Q_UNLIKELY(d->compressionFormatOrder.empty())) {
179 d->compressionFormatOrder = defaultCompressionFormatOrder;
180 qCWarning(C_STATICCOMPRESSED)
181 <<
"Invalid or empty value for compression_format_order. Has to be a string list "
182 "containing supported values. Using default value"
183 << defaultCompressionFormatOrder.join(u
',');
186 qCInfo(C_STATICCOMPRESSED) <<
"Supported compressions:" << supportedCompressions.join(u
',');
187 qCInfo(C_STATICCOMPRESSED) <<
"Compression format order:"
188 << d->compressionFormatOrder.join(u
',');
189 qCInfo(C_STATICCOMPRESSED) <<
"Include paths:" << d->includePaths;
192 d->beforePrepareAction(c, skipMethod);
198void StaticCompressedPrivate::beforePrepareAction(
Context *c,
bool *skipMethod)
205 const QString path = c->
req()->path().mid(1);
207 for (
const QString &dir : std::as_const(dirs)) {
208 if (path.startsWith(dir)) {
209 if (!locateCompressedFile(c, path)) {
213 res->
setBody(u
"File not found: "_qs + path);
225 const QRegularExpression _re = re;
226 const QRegularExpressionMatch match = _re.match(path);
227 if (match.hasMatch() && locateCompressedFile(c, path)) {
232bool StaticCompressedPrivate::locateCompressedFile(
Context *c,
const QString &relPath)
const
234 for (
const QDir &includePath : includePaths) {
235 qCDebug(C_STATICCOMPRESSED)
236 <<
"Trying to find" << relPath <<
"in" << includePath.absolutePath();
237 const QString path = includePath.absoluteFilePath(relPath);
238 const QFileInfo fileInfo(path);
239 if (fileInfo.exists()) {
241 const QDateTime currentDateTime = fileInfo.lastModified();
247 static QMimeDatabase db;
249 const QMimeType mimeType = db.mimeTypeForFile(path, QMimeDatabase::MatchExtension);
250 QByteArray contentEncoding;
251 QString compressedPath;
252 QByteArray _mimeTypeName;
254 if (mimeType.isValid()) {
258 if (mimeType.isDefault()) {
259 if (path.endsWith(u
"css.map", Qt::CaseInsensitive) ||
260 path.endsWith(u
"js.map", Qt::CaseInsensitive)) {
261 _mimeTypeName =
"application/json"_qba;
265 if (mimeTypes.contains(mimeType.name(), Qt::CaseInsensitive) ||
266 suffixes.contains(fileInfo.completeSuffix(), Qt::CaseInsensitive)) {
268 const auto acceptEncoding = c->
req()->
header(
"Accept-Encoding");
270 for (
const QString &format : std::as_const(compressionFormatOrder)) {
271 if (!acceptEncoding.contains(format.toLatin1())) {
274#ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
275 if (format == QLatin1String(
"br")) {
276 compressedPath = locateCacheFile(path, currentDateTime, Brotli);
277 if (compressedPath.isEmpty()) {
280 qCDebug(C_STATICCOMPRESSED)
281 <<
"Serving brotli compressed data from" << compressedPath;
282 contentEncoding =
"br"_qba;
287#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZSTD
288 if (format == QLatin1String(
"zstd")) {
289 compressedPath = locateCacheFile(path, currentDateTime, Zstd);
290 if (compressedPath.isEmpty()) {
293 qCDebug(C_STATICCOMPRESSED)
294 <<
"Serving zstd compressed data from" << compressedPath;
295 contentEncoding =
"zstd"_qba;
300 if (format == QLatin1String(
"gzip")) {
301 compressedPath = locateCacheFile(
302 path, currentDateTime, useZopfli ? ZopfliGzip : Gzip);
303 if (compressedPath.isEmpty()) {
306 qCDebug(C_STATICCOMPRESSED)
307 <<
"Serving" << (useZopfli ?
"zopfli" :
"default")
308 <<
"compressed gzip data from" << compressedPath;
309 contentEncoding =
"gzip"_qba;
312 }
else if (format == QLatin1String(
"deflate")) {
313 compressedPath = locateCacheFile(
314 path, currentDateTime, useZopfli ? ZopfliDeflate : Deflate);
315 if (compressedPath.isEmpty()) {
318 qCDebug(C_STATICCOMPRESSED)
319 <<
"Serving" << (useZopfli ?
"zopfli" :
"default")
320 <<
"compressed deflate data from" << compressedPath;
321 contentEncoding =
"deflate"_qba;
331 QFile *file = !compressedPath.isEmpty() ?
new QFile(compressedPath) : new QFile(path);
332 if (file->open(QFile::ReadOnly)) {
333 qCDebug(C_STATICCOMPRESSED) <<
"Serving" << path;
341 if (!_mimeTypeName.isEmpty()) {
343 }
else if (mimeType.isValid()) {
352 if (!contentEncoding.isEmpty()) {
356 qCDebug(C_STATICCOMPRESSED)
358 <<
"Original Size:" << fileInfo.size();
361 headers.
pushHeader(
"Vary"_qba,
"Accept-Encoding"_qba);
367 qCWarning(C_STATICCOMPRESSED) <<
"Could not serve" << path << file->errorString();
373 qCWarning(C_STATICCOMPRESSED) <<
"File not found" << relPath;
377QString StaticCompressedPrivate::locateCacheFile(
const QString &origPath,
378 const QDateTime &origLastModified,
379 Compression compression)
const
381 QString compressedPath;
385 switch (compression) {
390#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZSTD
395#ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
402 suffix = u
".deflate"_qs;
405 Q_ASSERT_X(
false,
"locate cache file",
"invalid compression type");
409 if (checkPreCompressed) {
410 const QFileInfo origCompressed(origPath + suffix);
411 if (origCompressed.exists()) {
412 compressedPath = origCompressed.absoluteFilePath();
413 return compressedPath;
417 if (onTheFlyCompression) {
419 const QString path = cacheDir.absoluteFilePath(
421 QCryptographicHash::hash(origPath.toUtf8(), QCryptographicHash::Md5).toHex()) +
423 const QFileInfo info(path);
425 if (info.exists() && (info.lastModified() > origLastModified)) {
426 compressedPath = path;
428 QLockFile lock(path + QLatin1String(
".lock"));
429 if (lock.tryLock(std::chrono::milliseconds{10})) {
430 switch (compression) {
431#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZSTD
433 if (compressZstd(origPath, path)) {
434 compressedPath = path;
438#ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
440 if (compressBrotli(origPath, path)) {
441 compressedPath = path;
446#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
447 if (compressZopfli(origPath, path, ZopfliFormat::ZOPFLI_FORMAT_GZIP)) {
448 compressedPath = path;
453 if (compressGzip(origPath, path, origLastModified)) {
454 compressedPath = path;
458#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
459 if (compressZopfli(origPath, path, ZopfliFormat::ZOPFLI_FORMAT_ZLIB)) {
460 compressedPath = path;
465 if (compressDeflate(origPath, path)) {
466 compressedPath = path;
477 return compressedPath;
480void StaticCompressedPrivate::loadZlibConfig(
const QVariantMap &conf)
483 zlib.compressionLevel =
484 conf.value(u
"zlib_compression_level"_qs,
485 defaultConfig.value(u
"zlib_compression_level"_qs, zlib.compressionLevelDefault))
488 if (!ok || zlib.compressionLevel < zlib.compressionLevelMin ||
489 zlib.compressionLevel > zlib.compressionLevelMax) {
490 qCWarning(C_STATICCOMPRESSED).nospace()
491 <<
"Invalid value set for zlib_compression_level. Value hat to be between "
492 << zlib.compressionLevelMin <<
" and " << zlib.compressionLevelMax
493 <<
" inclusive. Using default value " << zlib.compressionLevelDefault;
494 zlib.compressionLevel = zlib.compressionLevelDefault;
498static constexpr std::array<quint32, 256> crc32Tab = []() {
499 std::array<quint32, 256> tab{0};
500 for (std::size_t n = 0; n < 256; n++) {
501 auto c =
static_cast<quint32
>(n);
502 for (
int k = 0; k < 8; k++) {
504 c = 0xedb88320L ^ (c >> 1);
514quint32 updateCRC32(
unsigned char ch, quint32 crc)
517 return crc32Tab[(crc ^ ch) & 0xff] ^ (crc >> 8);
520quint32 crc32buf(
const QByteArray &data)
522 return ~std::accumulate(data.begin(),
525 [](quint32 oldcrc32,
char buf) {
526 return updateCRC32(
static_cast<unsigned char>(buf), oldcrc32);
530bool StaticCompressedPrivate::compressGzip(
const QString &inputPath,
531 const QString &outputPath,
532 const QDateTime &origLastModified)
const
534 qCDebug(C_STATICCOMPRESSED) <<
"Compressing" << inputPath <<
"with gzip to" << outputPath;
536 QFile input(inputPath);
537 if (Q_UNLIKELY(!input.open(QIODevice::ReadOnly))) {
538 qCWarning(C_STATICCOMPRESSED)
539 <<
"Can not open input file to compress with gzip:" << inputPath;
543 const QByteArray data = input.readAll();
544 if (Q_UNLIKELY(data.isEmpty())) {
545 qCWarning(C_STATICCOMPRESSED)
546 <<
"Can not read input file or input file is empty:" << inputPath;
551 QByteArray compressedData = qCompress(data, zlib.compressionLevel);
554 QFile output(outputPath);
555 if (Q_UNLIKELY(!output.open(QIODevice::WriteOnly))) {
556 qCWarning(C_STATICCOMPRESSED)
557 <<
"Can not open output file to compress with gzip:" << outputPath;
561 if (Q_UNLIKELY(compressedData.isEmpty())) {
562 qCWarning(C_STATICCOMPRESSED)
563 <<
"Failed to compress file with gzip, compressed data is empty:" << inputPath;
564 if (output.exists()) {
565 if (Q_UNLIKELY(!output.remove())) {
566 qCWarning(C_STATICCOMPRESSED)
567 <<
"Can not remove invalid compressed gzip file:" << outputPath;
575 compressedData.remove(0, 6);
576 compressedData.chop(4);
579 QDataStream headerStream(&header, QIODevice::WriteOnly);
582 headerStream << quint8(0x1f) << quint8(0x8b)
585 <<
static_cast<quint32
>(origLastModified.toSecsSinceEpoch())
589#elif defined Q_OS_MACOS
591#elif defined Q_OS_WIN
600 auto crc = crc32buf(data);
601 auto inSize = data.size();
603 QDataStream footerStream(&footer, QIODevice::WriteOnly);
604 footerStream << static_cast<quint8>(crc % 256) <<
static_cast<quint8
>((crc >> 8) % 256)
605 <<
static_cast<quint8
>((crc >> 16) % 256) <<
static_cast<quint8
>((crc >> 24) % 256)
606 <<
static_cast<quint8
>(inSize % 256) <<
static_cast<quint8
>((inSize >> 8) % 256)
607 <<
static_cast<quint8
>((inSize >> 16) % 256)
608 <<
static_cast<quint8
>((inSize >> 24) % 256);
610 if (Q_UNLIKELY(output.write(header + compressedData + footer) < 0)) {
611 qCCritical(C_STATICCOMPRESSED).nospace()
612 <<
"Failed to write compressed gzip file " << inputPath <<
": " << output.errorString();
619bool StaticCompressedPrivate::compressDeflate(
const QString &inputPath,
620 const QString &outputPath)
const
622 qCDebug(C_STATICCOMPRESSED) <<
"Compressing" << inputPath <<
"with deflate to" << outputPath;
624 QFile input(inputPath);
625 if (Q_UNLIKELY(!input.open(QIODevice::ReadOnly))) {
626 qCWarning(C_STATICCOMPRESSED)
627 <<
"Can not open input file to compress with deflate:" << inputPath;
631 const QByteArray data = input.readAll();
632 if (Q_UNLIKELY(data.isEmpty())) {
633 qCWarning(C_STATICCOMPRESSED)
634 <<
"Can not read input file or input file is empty:" << inputPath;
639 QByteArray compressedData = qCompress(data, zlib.compressionLevel);
642 QFile output(outputPath);
643 if (Q_UNLIKELY(!output.open(QIODevice::WriteOnly))) {
644 qCWarning(C_STATICCOMPRESSED)
645 <<
"Can not open output file to compress with deflate:" << outputPath;
649 if (Q_UNLIKELY(compressedData.isEmpty())) {
650 qCWarning(C_STATICCOMPRESSED)
651 <<
"Failed to compress file with deflate, compressed data is empty:" << inputPath;
652 if (output.exists()) {
653 if (Q_UNLIKELY(!output.remove())) {
654 qCWarning(C_STATICCOMPRESSED)
655 <<
"Can not remove invalid compressed deflate file:" << outputPath;
662 compressedData.remove(0, 4);
664 if (Q_UNLIKELY(output.write(compressedData) < 0)) {
665 qCCritical(C_STATICCOMPRESSED).nospace() <<
"Failed to write compressed deflate file "
666 << inputPath <<
": " << output.errorString();
673#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZOPFLI
674void StaticCompressedPrivate::loadZopfliConfig(
const QVariantMap &conf)
676 useZopfli = conf.value(u
"use_zopfli"_qs, defaultConfig.value(u
"use_zopfli"_qs,
false)).toBool();
678 ZopfliInitOptions(&zopfli.options);
680 zopfli.options.numiterations =
681 conf.value(u
"zopfli_iterations"_qs,
682 defaultConfig.value(u
"zopfli_iterations"_qs, zopfli.iterationsDefault))
684 if (!ok || zopfli.options.numiterations < zopfli.iterationsMin) {
685 qCWarning(C_STATICCOMPRESSED).nospace()
686 <<
"Invalid value set for zopfli_iterations. Value has to to be an integer value "
687 "greater than or equal to "
688 << zopfli.iterationsMin <<
". Using default value " << zopfli.iterationsDefault;
689 zopfli.options.numiterations = zopfli.iterationsDefault;
694bool StaticCompressedPrivate::compressZopfli(
const QString &inputPath,
695 const QString &outputPath,
696 ZopfliFormat format)
const
698 qCDebug(C_STATICCOMPRESSED) <<
"Compressing" << inputPath <<
"with zopfli to" << outputPath;
700 QFile input(inputPath);
701 if (Q_UNLIKELY(!input.open(QIODevice::ReadOnly))) {
702 qCWarning(C_STATICCOMPRESSED)
703 <<
"Can not open input file to compress with zopfli:" << inputPath;
707 const QByteArray data = input.readAll();
708 if (Q_UNLIKELY(data.isEmpty())) {
709 qCWarning(C_STATICCOMPRESSED)
710 <<
"Can not read input file or input file is empty:" << inputPath;
716 unsigned char *out{
nullptr};
719 ZopfliCompress(&zopfli.options,
721 reinterpret_cast<const unsigned char *
>(data.constData()),
726 if (Q_UNLIKELY(outSize <= 0)) {
727 qCWarning(C_STATICCOMPRESSED)
728 <<
"Failed to compress file with zopfli, compressed data is empty:" << inputPath;
733 QFile output{outputPath};
734 if (Q_UNLIKELY(!output.open(QIODeviceBase::WriteOnly))) {
735 qCWarning(C_STATICCOMPRESSED) <<
"Failed to open output file" << outputPath
736 <<
"for zopfli compression:" << output.errorString();
741 if (Q_UNLIKELY(output.write(
reinterpret_cast<const char *
>(out), outSize) < 0)) {
742 if (output.exists()) {
743 if (Q_UNLIKELY(!output.remove())) {
744 qCWarning(C_STATICCOMPRESSED)
745 <<
"Can not remove invalid compressed zopfli file:" << outputPath;
748 qCWarning(C_STATICCOMPRESSED) <<
"Failed to write zopfli compressed data to output file"
749 << outputPath <<
":" << output.errorString();
760#ifdef CUTELYST_STATICCOMPRESSED_WITH_BROTLI
761void StaticCompressedPrivate::loadBrotliConfig(
const QVariantMap &conf)
764 brotli.qualityLevel =
765 conf.value(u
"brotli_quality_level"_qs,
766 defaultConfig.value(u
"brotli_quality_level"_qs, brotli.qualityLevelDefault))
769 if (!ok || brotli.qualityLevel < BROTLI_MIN_QUALITY ||
770 brotli.qualityLevel > BROTLI_MAX_QUALITY) {
771 qCWarning(C_STATICCOMPRESSED).nospace()
772 <<
"Invalid value for brotli_quality_level. "
773 "Has to be an integer value between "
774 << BROTLI_MIN_QUALITY <<
" and " << BROTLI_MAX_QUALITY
775 <<
" inclusive. Using default value " << brotli.qualityLevelDefault;
776 brotli.qualityLevel = brotli.qualityLevelDefault;
780bool StaticCompressedPrivate::compressBrotli(
const QString &inputPath,
781 const QString &outputPath)
const
783 qCDebug(C_STATICCOMPRESSED) <<
"Compressing" << inputPath <<
"with brotli to" << outputPath;
785 QFile input(inputPath);
786 if (Q_UNLIKELY(!input.open(QIODevice::ReadOnly))) {
787 qCWarning(C_STATICCOMPRESSED)
788 <<
"Can not open input file to compress with brotli:" << inputPath;
792 const QByteArray data = input.readAll();
793 if (Q_UNLIKELY(data.isEmpty())) {
794 qCWarning(C_STATICCOMPRESSED)
795 <<
"Can not read input file or input file is empty:" << inputPath;
801 size_t outSize = BrotliEncoderMaxCompressedSize(
static_cast<size_t>(data.size()));
802 if (Q_UNLIKELY(outSize == 0)) {
803 qCWarning(C_STATICCOMPRESSED) <<
"Needed output buffer too large to compress input of size"
804 << data.size() <<
"with brotli";
807 QByteArray outData{
static_cast<qsizetype
>(outSize), Qt::Uninitialized};
809 const auto in =
reinterpret_cast<const uint8_t *
>(data.constData());
810 auto out =
reinterpret_cast<uint8_t *
>(outData.data());
812 const BROTLI_BOOL status = BrotliEncoderCompress(brotli.qualityLevel,
813 BROTLI_DEFAULT_WINDOW,
819 if (Q_UNLIKELY(status != BROTLI_TRUE)) {
820 qCWarning(C_STATICCOMPRESSED) <<
"Failed to compress" << inputPath <<
"with brotli";
824 outData.resize(
static_cast<qsizetype
>(outSize));
826 QFile output{outputPath};
827 if (Q_UNLIKELY(!output.open(QIODeviceBase::WriteOnly))) {
828 qCWarning(C_STATICCOMPRESSED) <<
"Failed to open output file" << outputPath
829 <<
"for brotli compression:" << output.errorString();
833 if (Q_UNLIKELY(output.write(outData) < 0)) {
834 if (output.exists()) {
835 if (Q_UNLIKELY(!output.remove())) {
836 qCWarning(C_STATICCOMPRESSED)
837 <<
"Can not remove invalid compressed brotli file:" << outputPath;
840 qCWarning(C_STATICCOMPRESSED) <<
"Failed to write brotli compressed data to output file"
841 << outputPath <<
":" << output.errorString();
849#ifdef CUTELYST_STATICCOMPRESSED_WITH_ZSTD
850bool StaticCompressedPrivate::loadZstdConfig(
const QVariantMap &conf)
852 zstd.ctx = ZSTD_createCCtx();
854 qCCritical(C_STATICCOMPRESSED) <<
"Failed to create Zstandard compression context";
860 zstd.compressionLevel =
861 conf.value(u
"zstd_compression_level"_qs,
862 defaultConfig.value(u
"zstd_compression_level"_qs, zstd.compressionLevelDefault))
864 if (!ok || zstd.compressionLevel < ZSTD_minCLevel() ||
865 zstd.compressionLevel > ZSTD_maxCLevel()) {
866 qCWarning(C_STATICCOMPRESSED).nospace()
867 <<
"Invalid value for zstd_compression_level. Has to be an integer value between "
868 << ZSTD_minCLevel() <<
" and " << ZSTD_maxCLevel() <<
" inclusive. Using default value "
869 << zstd.compressionLevelDefault;
870 zstd.compressionLevel = zstd.compressionLevelDefault;
876bool StaticCompressedPrivate::compressZstd(
const QString &inputPath,
877 const QString &outputPath)
const
879 qCDebug(C_STATICCOMPRESSED) <<
"Compressing" << inputPath <<
"with zstd to" << outputPath;
881 QFile input{inputPath};
882 if (Q_UNLIKELY(!input.open(QIODeviceBase::ReadOnly))) {
883 qCWarning(C_STATICCOMPRESSED)
884 <<
"Can not open input file to compress with zstd:" << inputPath;
888 const QByteArray inData = input.readAll();
889 if (Q_UNLIKELY(inData.isEmpty())) {
890 qCWarning(C_STATICCOMPRESSED)
891 <<
"Can not read input file or input file is empty:" << inputPath;
897 const size_t outBufSize = ZSTD_compressBound(
static_cast<size_t>(inData.size()));
898 if (Q_UNLIKELY(ZSTD_isError(outBufSize) == 1)) {
899 qCWarning(C_STATICCOMPRESSED)
900 <<
"Failed to compress" << inputPath <<
"with zstd:" << ZSTD_getErrorName(outBufSize);
903 QByteArray outData{
static_cast<qsizetype
>(outBufSize), Qt::Uninitialized};
905 auto outDataP =
static_cast<void *
>(outData.data());
906 auto inDataP =
static_cast<const void *
>(inData.constData());
908 const size_t outSize = ZSTD_compressCCtx(
909 zstd.ctx, outDataP, outBufSize, inDataP, inData.size(), zstd.compressionLevel);
910 if (Q_UNLIKELY(ZSTD_isError(outSize) == 1)) {
911 qCWarning(C_STATICCOMPRESSED)
912 <<
"Failed to compress" << inputPath <<
"with zstd:" << ZSTD_getErrorName(outSize);
916 outData.resize(
static_cast<qsizetype
>(outSize));
918 QFile output{outputPath};
919 if (Q_UNLIKELY(!output.open(QIODeviceBase::WriteOnly))) {
920 qCWarning(C_STATICCOMPRESSED) <<
"Failed to open output file" << outputPath
921 <<
"for zstd compression:" << output.errorString();
925 if (Q_UNLIKELY(output.write(outData) < 0)) {
926 if (output.exists()) {
927 if (Q_UNLIKELY(!output.remove())) {
928 qCWarning(C_STATICCOMPRESSED)
929 <<
"Can not remove invalid compressed zstd file:" << outputPath;
932 qCWarning(C_STATICCOMPRESSED) <<
"Failed to write zstd compressed data to output file"
933 << outputPath <<
":" << output.errorString();
941#include "moc_staticcompressed.cpp"
The Cutelyst application.
Engine * engine() const noexcept
void beforePrepareAction(Cutelyst::Context *c, bool *skipMethod)
QVariant config(const QString &key, const QVariant &defaultValue={}) const
Response * res() const noexcept
Response * response() const noexcept
QVariantMap config(const QString &entity) const
Base class for Cutelyst Plugins.
Headers headers() const noexcept
QByteArray header(QByteArrayView key) const noexcept
void setContentType(const QByteArray &type)
void setStatus(quint16 status) noexcept
void setBody(QIODevice *body)
Headers & headers() noexcept
Serve static files compressed on the fly or pre-compressed.
~StaticCompressed() override
void setServeDirsOnly(bool dirsOnly)
void setIncludePaths(const QStringList &paths)
void setDirs(const QStringList &dirs)
StaticCompressed(Application *parent)
bool setup(Application *app) override
The Cutelyst namespace holds all public Cutelyst API.