diff --git a/examples/Upload/Upload.ino b/examples/Upload/Upload.ino index ec2a0868..d5eb3ade 100644 --- a/examples/Upload/Upload.ino +++ b/examples/Upload/Upload.ino @@ -63,6 +63,7 @@ void setup() { if (!buffer->reserve(size)) { delete buffer; request->abort(); + return; } request->_tempObject = buffer; } @@ -100,6 +101,7 @@ void setup() { if (!request->_tempFile) { request->send(400, "text/plain", "File not available for writing"); + return; } } if (len) { @@ -141,6 +143,7 @@ void setup() { // first pass ? if (!index) { + // Note: using content type to determine size is not reliable! size_t size = request->header("Content-Length").toInt(); if (!size) { request->send(400, "text/plain", "No Content-Length"); @@ -150,6 +153,7 @@ void setup() { if (!buffer) { // not enough memory request->abort(); + return; } else { request->_tempObject = buffer; } diff --git a/examples/UploadFlash/UploadFlash.ino b/examples/UploadFlash/UploadFlash.ino new file mode 100644 index 00000000..c3f949e2 --- /dev/null +++ b/examples/UploadFlash/UploadFlash.ino @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov + +// +// Demo to upload a firmware and filesystem image via multipart form data +// + +#include +#if defined(ESP32) || defined(LIBRETINY) +#include +#include +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include +#include +#endif + +#include +#include +#include + +// ESP32 example ONLY +#ifdef ESP32 +#include +#endif + +static AsyncWebServer server(80); + +void setup() { + Serial.begin(115200); + + if (!LittleFS.begin()) { + LittleFS.format(); + LittleFS.begin(); + } + +#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI || CONFIG_ESP32_WIFI_ENABLED + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); +#endif + +// ESP32 example ONLY +#ifdef ESP32 + + // Shows how to get the fw and fs (names) and filenames from a multipart upload, + // and also how to handle multiple file uploads in a single request. + // + // This example also shows how to pass and handle different parameters having the same name in query string, post form and content-disposition. + // + // Execute in the terminal, in order: + // + // 1. Build firmware: pio run -e arduino-3 + // 2. Build FS image: pio run -e arduino-3 -t buildfs + // 3. Flash both at the same time: curl -v -F "name=Bob" -F "fw=@.pio/build/arduino-3/firmware.bin" -F "fs=@.pio/build/arduino-3/littlefs.bin" http://192.168.4.1/flash?name=Bill + // + server.on( + "/flash", HTTP_POST, + [](AsyncWebServerRequest *request) { + if (request->getResponse()) { + // response already created + return; + } + + // list all parameters + Serial.println("Request parameters:"); + const size_t params = request->params(); + for (size_t i = 0; i < params; i++) { + const AsyncWebParameter *p = request->getParam(i); + Serial.printf("Param[%u]: %s=%s, isPost=%d, isFile=%d, size=%u\n", i, p->name().c_str(), p->value().c_str(), p->isPost(), p->isFile(), p->size()); + } + + Serial.println("Flash / Filesystem upload completed"); + + request->send(200, "text/plain", "Upload complete"); + }, + [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + Serial.printf("Upload[%s]: index=%u, len=%u, final=%d\n", filename.c_str(), index, len, final); + + if (request->getResponse() != nullptr) { + // upload aborted + return; + } + + // start a new content-disposition upload + if (!index) { + // list all parameters + const size_t params = request->params(); + for (size_t i = 0; i < params; i++) { + const AsyncWebParameter *p = request->getParam(i); + Serial.printf("Param[%u]: %s=%s, isPost=%d, isFile=%d, size=%u\n", i, p->name().c_str(), p->value().c_str(), p->isPost(), p->isFile(), p->size()); + } + + // get the content-disposition parameter + const AsyncWebParameter *p = request->getParam(asyncsrv::T_name, true, true); + if (p == nullptr) { + request->send(400, "text/plain", "Missing content-disposition 'name' parameter"); + return; + } + + // determine upload type based on the parameter name + if (p->value() == "fs") { + Serial.printf("Filesystem image upload for file: %s\n", filename.c_str()); + if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) { + Update.printError(Serial); + request->send(400, "text/plain", "Update begin failed"); + return; + } + + } else if (p->value() == "fw") { + Serial.printf("Firmware image upload for file: %s\n", filename.c_str()); + if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) { + Update.printError(Serial); + request->send(400, "text/plain", "Update begin failed"); + return; + } + + } else { + Serial.printf("Unknown upload type for file: %s\n", filename.c_str()); + request->send(400, "text/plain", "Unknown upload type"); + return; + } + } + + // some bytes to write ? + if (len) { + if (Update.write(data, len) != len) { + Update.printError(Serial); + Update.end(); + request->send(400, "text/plain", "Update write failed"); + return; + } + } + + // finish the content-disposition upload + if (final) { + if (!Update.end(true)) { + Update.printError(Serial); + request->send(400, "text/plain", "Update end failed"); + return; + } + + // success response is created in the final request handler when all uploads are completed + Serial.printf("Upload success of file %s\n", filename.c_str()); + } + } + ); + +#endif + + server.begin(); +} + +// not needed +void loop() { + delay(100); +} diff --git a/platformio.ini b/platformio.ini index dd50c1bd..db36e4e6 100644 --- a/platformio.ini +++ b/platformio.ini @@ -34,6 +34,7 @@ src_dir = examples/PerfTests ; src_dir = examples/StaticFile ; src_dir = examples/Templates ; src_dir = examples/Upload +; src_dir = examples/UploadFlash ; src_dir = examples/URIMatcher ; src_dir = examples/URIMatcherTest ; src_dir = examples/WebSocket diff --git a/src/WebRequest.cpp b/src/WebRequest.cpp index a0b7e9cb..aa5cd30b 100644 --- a/src/WebRequest.cpp +++ b/src/WebRequest.cpp @@ -520,6 +520,16 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) { _itemFilename = nameVal; _itemIsFile = true; } + // Add the parameters from the content-disposition header to the param list, flagged as POST and File, + // so that they can be retrieved using getParam(name, isPost=true, isFile=true) + // in the upload handler to correctly handle multiple file uploads within the same request. + // Example: Content-Disposition: form-data; name="fw"; filename="firmware.bin" + // See: https://github.com/ESP32Async/ESPAsyncWebServer/discussions/328 + if (_itemIsFile && _itemName.length() && _itemFilename.length()) { + // add new parameters for this content-disposition + _params.emplace_back(T_name, _itemName, true, true); + _params.emplace_back(T_filename, _itemFilename, true, true); + } } _temp = emptyString; } else { @@ -593,6 +603,10 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) { } _itemBufferIndex = 0; _params.emplace_back(_itemName, _itemFilename, true, true, _itemSize); + // remove previous occurrence(s) of content-disposition parameters for this upload + _params.remove_if([this](const AsyncWebParameter &p) { + return p.isPost() && p.isFile() && (p.name() == T_name || p.name() == T_filename); + }); free(_itemBuffer); _itemBuffer = NULL; }