|
| 1 | +#include <ESPAsyncWebServer.h> |
| 2 | + |
| 3 | +/** |
| 4 | + * @brief Sends a file from the filesystem to the client, with optional gzip compression and ETag-based caching. |
| 5 | + * |
| 6 | + * This method serves files over HTTP from the provided filesystem. If a compressed version of the file |
| 7 | + * (with a `.gz` extension) exists and the `download` flag is not set, it serves the compressed file. |
| 8 | + * It also handles ETag caching using the CRC32 value from the gzip trailer, responding with `304 Not Modified` |
| 9 | + * if the client's `If-None-Match` header matches the generated ETag. |
| 10 | + * |
| 11 | + * @param fs Reference to the filesystem (SPIFFS, LittleFS, etc.). |
| 12 | + * @param path Path to the file to be served. |
| 13 | + * @param contentType Optional MIME type of the file to be sent. |
| 14 | + * If contentType is "" it will be obtained from the file extension |
| 15 | + * @param download If true, forces the file to be sent as a download (disables gzip compression). |
| 16 | + * @param callback Optional template processor for dynamic content generation. |
| 17 | + * Templates will not be processed in compressed files. |
| 18 | + * |
| 19 | + * @note If neither the file nor its compressed version exists, responds with `404 Not Found`. |
| 20 | + */ |
| 21 | +void AsyncWebServerRequest::send(FS &fs, const String &path, const char *contentType, bool download, AwsTemplateProcessor callback) { |
| 22 | + const String gzPath = path + asyncsrv::T__gz; |
| 23 | + const bool useCompressedVersion = !download && fs.exists(gzPath); |
| 24 | + |
| 25 | + // If-None-Match header |
| 26 | + if (useCompressedVersion && this->hasHeader(asyncsrv::T_INM)) { |
| 27 | + // CRC32-based ETag of the trailer, bytes 4-7 from the end |
| 28 | + File file = fs.open(gzPath, fs::FileOpenMode::read); |
| 29 | + if (file && file.size() >= 18) { // 18 is the minimum size of valid gzip file |
| 30 | + file.seek(file.size() - 8); |
| 31 | + |
| 32 | + uint8_t crcFromGzipTrailer[4]; |
| 33 | + if (file.read(crcFromGzipTrailer, sizeof(crcFromGzipTrailer)) == sizeof(crcFromGzipTrailer)) { |
| 34 | + char serverETag[9]; |
| 35 | + _getEtag(crcFromGzipTrailer, serverETag); |
| 36 | + |
| 37 | + // Compare with client's If-None-Match header |
| 38 | + const AsyncWebHeader *inmHeader = this->getHeader(asyncsrv::T_INM); |
| 39 | + if (inmHeader && inmHeader->value().equals(serverETag)) { |
| 40 | + file.close(); |
| 41 | + this->send(304); // Not Modified |
| 42 | + return; |
| 43 | + } |
| 44 | + } |
| 45 | + file.close(); |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + // If we get here, create and send the normal response |
| 50 | + if (fs.exists(path) || useCompressedVersion) { |
| 51 | + send(beginResponse(fs, path, contentType, download, callback)); |
| 52 | + } else { |
| 53 | + send(404); |
| 54 | + } |
| 55 | +} |
| 56 | + |
| 57 | +/** |
| 58 | + * @brief Generates an ETag string from a 4-byte trailer |
| 59 | + * |
| 60 | + * This function converts a 4-byte array into a hexadecimal ETag string enclosed in quotes. |
| 61 | + * |
| 62 | + * @param trailer[4] Input array of 4 bytes to convert to hexadecimal |
| 63 | + * @param serverETag Output buffer to store the ETag |
| 64 | + * Must be pre-allocated with minimum 9 bytes (8 hex + 1 null terminator) |
| 65 | + */ |
| 66 | +void AsyncWebServerRequest::_getEtag(uint8_t trailer[4], char *serverETag) { |
| 67 | + static constexpr char hexChars[] = "0123456789ABCDEF"; |
| 68 | + |
| 69 | + uint32_t data; |
| 70 | + memcpy(&data, trailer, 4); |
| 71 | + |
| 72 | + serverETag[0] = hexChars[(data >> 4) & 0x0F]; |
| 73 | + serverETag[1] = hexChars[data & 0x0F]; |
| 74 | + serverETag[2] = hexChars[(data >> 12) & 0x0F]; |
| 75 | + serverETag[3] = hexChars[(data >> 8) & 0x0F]; |
| 76 | + serverETag[4] = hexChars[(data >> 20) & 0x0F]; |
| 77 | + serverETag[5] = hexChars[(data >> 16) & 0x0F]; |
| 78 | + serverETag[6] = hexChars[(data >> 28)]; |
| 79 | + serverETag[7] = hexChars[(data >> 24) & 0x0F]; |
| 80 | + serverETag[8] = '\0'; |
| 81 | +} |
0 commit comments