Motivation

Bei der Nutzung des AsyncWebServer ist bei Operationen mit Dateien Vorsicht geboten:

Die Klasse UrsAsyncFSHelper stellt statische Methoden zur Vermeidung dieser Probleme bereit.

In­halts­ver­zeich­nis

Bereich UrsAsyncFSHelper

Enumeration UrsFileActionResultCode

Struktur UrsFileActionResult

Callbackfunktionstyp UrsFileActionCallback_t

Klasse UrsAsyncFSHelper

fileUploadHandler

removeFile

readFile2String


Bereich UrsAsyncFSHelper

Der Bereich UrsAsyncFSHelper stellt Typen, Methoden und Konstanten bereit, die den Umgang mit Dateien im Zusammenhang mit der Benutzung des ESPAsyncWebServer bzw. dem UrsAyncWebserver erleichtern. Der Bereich wird aus drei Klassen gebildet:

Enumeration UrsFileActionResultCode

Diese Enumeration liefert die Rückgabe-Werte (Return-Codes) für Datei-Operationen.

// Ergebnis der letzten Datei-Operation
enum class UrsFileActionResultCode {
  None,     // keine gemeldete Datei-Aktion
  Uploaded, // erfolgreicher Upload durchgeführt
  ErrorUp,  // Upload mit Fehler abgebrochen
  Deleted,  // Datei erfolgreich gelöscht
  ErrorDel  // Fehler beim Datei-Löschen
};

UrsFileActionResultCode::None kann als Default-Wert genutzt werden.

Struktur UrsFileActionResult

Diese Struktur wird zur Rückmeldung des Ergebnisses der letzten Datei-Operation benutzt.

#define SPIFFS_OBJ_NAME_LEN 32
// Zur Übergabe von Return-Code und Dateiname der letzten Datei-Operation
struct UrsFileActionResult {
  UrsFileActionResultCode actionCode; // Return-Code der letzten Operation
  char fileName[SPIFFS_OBJ_NAME_LEN]; // Betroffene Datei

  // Befüllt die Felder
  static void fillUrsFileActionResult(UrsFileActionResult* ar, UrsFileActionResultCode code, String fn);
};

SPIFFS_OBJ_NAME_LEN in "spiffs-config.h" legt die maximale Länge eines Dateinamens fest. "spiffs-config.h" kann leider nicht eingebunden werden, da dabei Typ-Konflikte entstehen. Deshalb hier eine Kopie der Definition. In der Version 2.4.2 des ESP8266-Plugin beträgt der Wert für SPIFFS_OBJ_NAME_LEN 32 Byte (31 Zeichen + \0).

Diese Struktur ist absichtlich nicht objektorientiert ausgelegt (z.B. Konstruktor oder ein String-Objekt für den Dateinamen). Objekte mit Objekten als Elemente ist auf Grund des Übergabe-Mechanismus der internen Speicherverwaltung des  ESPAsyncWebServer nicht möglich (siehe ESPAsyncWebServer: Request Life Cycle, die Anmerkung zu _tempObject in Body data handling und weiter unten den Abschnitt zum fileUploadHandler):

Wenn man den File-Upload-Mechanismus des ESPAsyncWebServer nutzt, wertet der Server intern die Request-Header und -Parameter aus. Ist ein File-Upload vorgesehen, wird zunächst der Upload-Handler mit den passenden Angaben aufgerufen. Danach wird, wie sonst auch, der Request-Handler aufgerufen. Das zugehörige AsyncWebServerRequest-Objekt liegt sowohl im Upload- als auch im Request-Handler vor. Dieses Objekt enthält den (relativ) frei benutzbaren void-Pointer _tempObject, über den eigene Daten vom Upload- an den Request-Handler weiter geleitet werden können. Wenn der Request beendet ist, löscht der ESPAsyncWebServer evtl. hinterlegte Daten (_tempObject != nullptr) per free. free gibt nur den allokierten Speicher frei, ruft aber keinen Destruktor auf. Daten (-bereiche), die über diesen Mechanismus übergeben werden sollen, müssen also per malloc angefordert werden (nicht per new!). malloc ruft auch keinen Konstruktor auf, wie new es macht.

Wird _tempObject nicht als Zeiger auf einem mit malloc allokierten Speicherbereich genutzt, muss(!) dieser vor Verlassen des Request-Handlers auf NULL (nullptr) gesetzt werden. Ansonsten würde das ausgeführte free die Struktur des Heap zerstören!

Der vorgefertigte SPIFFS-Editor nutzt diesen Mechanismus.

Callbackfunktionstyp EvUrsFileActionCallback

Dieser Funktionstyp legt den Aufbau der Rückruffunktionen fest, die von fileUploadHandler und removeFile nach Beendigung der Funktion aufgerufen werden.

// Funktionstyp die Rückruf-Funktion nach Ausführung einer Datei-Aktion
typedef std::function<void(String& fileName, UrsFileActionResultCode lastActionResult)> EvUrsFileActionCallback;

Der String fileName wird als Referenz übergeben, um unnötige Kopieraktionen zu vermeiden. Eine Änderung durch die aufgerufene Methode ist jedoch nicht vorgesehen.

Beispiel:

// Callback-Funktion:
void onFileUploaded(String& Filename, UrsFileActionResultCode lastActionResult) {
  Serial.printf("Datei-Upload der Datei %s %s abgeschlossen\n", Filename.c_str(),
    lastActionResult == UrsFileActionResultCode::Uploaded ? "erfolgreich" : "fehlerhaft");
}

setup() {
  // ...
  UrsAsyncFSHelper::onFileUploaded(onFileUploaded); // Callback-Funktion registrieren
  // ...

Klasse UrsAsyncFSHelper

Funktionsübersicht

Die Klasse UrsAsyncFSHelper stellt folgende statische Methoden bereit:

fileUploadHandler
Methode Beschreibung Anmerkung
fileUploadHandler Handler-Methode zum Hochladen von Dateien ins SPIFFS. Kann bei einem Request-Handler für den AsyncWebServer für den automatisierten File-Upload eingebunden werden. Liefert das Upload-Ergebnis an den Request-Handler zurück.
Ruft die mit onFileUploaded registrierte Callback-Methode auf. So ist eine zentrale Benachrichtigung möglich.
onFileUploaded Registriert eine Callback-Funktion, die aufgerufen wird, wenn eine Datei hochgeladen wurde. Die registrierte Methode wird asynchron aufgerufen. Innerhalb dieser Methode darf yield (und damit z.B. auch delay) nicht aufgerufen werden; weder direkt, noch indirekt durch den Aufruf einer Methode, die ihrerseits yield aufruft. Siehe ESPAsyncWebServer: Important things to remember
removeFile Löscht eine Datei aus dem SPIFFS. Ruft die mit onFileRemoved registrierte Callback-Methode auf. So ist eine zentrale Benachrichtigung möglich.
onFileRemoved Registriert eine Callback-Funktion, die aufgerufen wird, wenn eine Datei gelöscht wurde. Die registrierte Methode wird asynchron aufgerufen. Innerhalb dieser Methode darf yield (und damit z.B. auch delay) nicht aufgerufen werden; weder direkt, noch indirekt durch den Aufruf einer Methode, die ihrerseits yield aufruft. Siehe ESPAsyncWebServer: Important things to remember
readFile2String Ersetzt File::readString (s. auch Stream::readString). File ist von Stream abgeleitet. Stream::readString ist auf das Auslesen der seriellen Schnittstelle ausgelegt. Es ist ein Timeout-Verfahren implementiert und dazu wird intern yield aufgerufen. Dies ist in den asynchronen Methoden der AsyncWebServer-Klasse nicht erlaubt.

fileUploadHandler

Diese Methode kann beim ESPAsyncWebServer als File-Upload-Handler registriert werden. Dies geschieht, wenn die Web-Site (abgeleitet von UrsAsyncWebSite) bei einer Instanz der Klasse UrsAsyncWebServer über die Methode registerWebSite registriert wird. Gesteuert wird das Einbinden von fileUploadHandler über den Parameter registerStdUploadHandler.

Wenn der Datei-Upload komplett ist, wird die per onFileUploaded registrierte Callback-Funktion aufgerufen. Die Funktion erhält als ersten Parameter den Dateinamen und als zweiten Parameter den Ergebnis-Code. Hier sind zwei Ausprägungen möglich: UrsFileActionResultCode::Uploaded und UrsFileActionResultCode::ErrorUp.

Der Upload-Handler übergibt an den nachfolgend aufgerufenen Request-Handler über den Zeiger request->_tempObject eine Struktur vom Typ UrsFileActionResult. Dieser Pointer muss überprüft werden! Nur wenn er ungleich nullptr ist, hat vorher ein Upload stattgefunden.

if (request->_tempObject) {
  auto ptr = (UrsFileActionResult*)(request->_tempObject);
  if (ptr->actionCode == UrsFileActionResultCode::Uploaded) {
    // ...
  }
}

Wenn der Pointer request->_tempObject nicht verändert wird, ist im Request-Handler keine weitere Aktion notwendig. Wenn er jedoch anderweitig benutzt werden soll, muss vorher der hinterlegte Speicherbereich gelöscht werden. Bevor am Ende des Request-Handlers request->send aufgerufen wird, muss dieser Zeiger der nullptr sein oder auf einen durch malloc allokierten Speicherbereich verweisen:

free(request->_tempObject);     // Bereits allokierten Speicher freigeben
// Was immer man mit der Variablen machen möchte
request->_tempObject = nullptr; // Zeiger ungültig machen
// oder
request->_tempObject = malloc(...);
// ...
request->send(...);

Der Handler überwacht beim Upload den noch zur Verfügung stehenden freien Speicher (man beachte den Sicherheitsaufschlag, s. hierzu ESP8266 spezielle Klassen: SPIFFS).

removeFile

removeFile löscht eine Datei per SPIFFS.remove und ruft anschließend die per onFileRemoved registrierte Callback-Funktion auf. Der Sinn dieser Methode ist es, eine zentrale Methode für diese Aufgabe zu haben und damit einen zentralen Informationsmechanismus (Callback). Mögliche Werte für den Return-Code sind UrsFileActionResultCode::Deleted und UrsFileActionResultCode::ErrorDel. Beispiel:

// Callback-Funktion:
void onFileRemoved(String& Filename, UrsFileActionResultCode lastActionResult) {
  if (lastActionResult == UrsFileActionResultCode::Uploaded){
    Serial.printf("Datei %s gelöscht\n", Filename.c_str());
  } else {
    Serial.printf("Fehler beim Löschen der Datei %s\n", Filename.c_str());
}

setup() {
  // ...
  UrsAsyncFSHelper::onFileRemoved(onFileRemoved); // Callback-Funktion registrieren
  // ...

readFile2String

Die Methode File::readString, die eine Datei in einen String einliest, kann im Kontext des AsyncWebServer nicht benutzt werden! In File::readString wird implizit yield aufgerufen. Dieser Aufruf führt zu einer Exception und zum Programmabbruch. readFile2String kommt ohne diesen Aufruf aus.

String textFileContent;
fs::File textFile = SPIFFS.open("\textdatei", "r");
if (textFile) {
  // textFileContent = textFile.readString(); <-- führt zum Programmabsturz
  textFileContent = UrsAsyncFSHelper::readFile2String(textFile);
  textFile.close();
}