通过http在Arduino ESP32上文件上传问题的任何解决方案

Any solution for file upload problem on Arduino ESP32 via http

首先是问题

用户可以使用 ajax 从 Web 上传文件。如果文件比较大,上传需要一段时间。如果用户的连接丢失或上传过程中出现问题,文件将被损坏或清空。

我应该如何保护上传过程,以便文件在由于某种原因失败时保持不变?

我在 Arduino ESP32 上使用以下库:

我的 esp32 上有一个基本的文件上传处理程序,如下所示:

server.on("/uploading", HTTP_POST, [](AsyncWebServerRequest * request) {
  }, handleFileUpload);

void handleFileUpload(AsyncWebServerRequest * request, String filename,size_t index, uint8_t *data, size_t len, bool final) {
  if (!index) {
    if (!filename.startsWith("/"))
      filename = "/" + filename;
    if (LITTLEFS.exists(filename)) {
      LITTLEFS.remove(filename);
    }
    uploadFile = LITTLEFS.open(filename, "w");
  }
  for (size_t i = 0; i < len; i++) {
    uploadFile.write(data[i]);
  }
  if (final) {
    uploadFile.close();
    if(filename == "/myHomeProgram.json"){initProgram = true;}
    AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "File Uploaded;"+filename);
    response->addHeader("Access-Control-Allow-Origin","*");
    request->send(response);
  }

}

这工作得很好,文件在 99% 的情况下都能正确上传,但如果失败,我会丢失文件数据,或者程序的其他部分想要打开相同的文件文件也失败了。 我是否应该写入一个临时文件,如果它成功将内容以某种方式写入目标文件? 这是客户端(JS)端的示例:

// Example call: 
saveFile(JSON.stringify(places),"/myHomeProgram.json","application/json");

function saveFile(data, filename, type) {
    var file = new Blob([data], {type: type});
    form = new FormData();
    form.append("blob", file, filename);
    $.ajax({
        url: '/uploading', 
        type: 'POST',
        data: form,
        processData: false,
        contentType: false
      }).done(function(resp){
        var response = resp.split(";");
        
        $(".saveIconGraph").removeClass("fas fa-spinner fa-spin");
        $(".saveIconGraph").addClass("far fa-save");

        if(response[1] == "/myHomeProgram.json"){
            toast("success","saveOk","progInfo",3500);
            showSaved();
            setTimeout(() => {
                $("#saveMe").fadeOut( "slow", function() { 
                    showSave();
                });
            }, 1000);
            initPlaces();
        }
      }).fail(function(resp){
        var response = resp.split(";");

        $(".saveIconGraph").removeClass("fas fa-spinner fa-spin");
        $(".saveIconGraph").addClass("far fa-save");
        
        if(response[1] == "/myHomeProgram.json"){
            toast("error","saveNotOk","progInfo",3500);
            showSaveError();
            $("#saveMeBtn").addClass("shakeEffect");
            setTimeout(() => {
                $("#saveMeBtn").removeClass("shakeEffect");
                showSave();
            }, 4500);
        }
      });
}

我可以在写入前将文件保存在临时 char 变量中,最后我可以匹配文件的大小和临时变量的大小,如果不相同,则回滚到上一个。这可以管理吗?

像这样:

String uploadTemp = "";
inline boolean saveFileToTemp(String fileName){
  uploadTemp = "";
  File f = LITTLEFS.open(fileName, "r");
  if (!f) {
    f.close();
    return false;
  }else{
    for (int i = 0; i < f.size(); i++){
      uploadTemp += (char)f.read();
    }
  }
  f.close();
  return true;
}

inline boolean revertBackFile(String fileName){
  File g = LITTLEFS.open(fileName, "w");
  if (!g) {
    g.close();
    return false;
  }else{
    g.print(uploadTemp);
  }
  g.close();
  return true;
}

inline boolean matchFileSizes(String fileName,boolean isFileExists){
  boolean isCorrect = false;
  if(isFileExists){
    File writedFile = LITTLEFS.open(fileName, "w");
    if( writedFile.size() == uploadTemp.length()){
      isCorrect = true;
    }else{
      isCorrect = false;
    }
    writedFile.close();
    return isCorrect;
  }else{
    return true;
  }
}

void handleFileUpload(AsyncWebServerRequest * request, String filename,size_t index, uint8_t *data, size_t len, bool final) {
  String webResponse;
  boolean error = false,isFileExists = false;
  if (!index) {
    if (!filename.startsWith("/"))
      filename = "/" + filename;
    if (LITTLEFS.exists(filename)) {
      isFileExists = true;
      // Save the file to a temporary String if it success we continue.
      if( saveFileToTemp(filename) ){
        LITTLEFS.remove(filename);
      }else{
        // If the file save was fail we abort everything.
        webResponse = "File NOT Uploaded " + filename;
        final = true;
        error = true;
      }
    }
    if( !error ){
      uploadFile = LITTLEFS.open(filename, "w");
    }
  }
  if( !error ){
    // Copy content to the actual file
    for (size_t i = 0; i < len; i++) {
      uploadFile.write(data[i]);
    }
  }
  if (final) {
    uploadFile.close();
    if( !error ){
      if( matchFileSizes(filename,isFileExists) ){
        if(filename == "/myHomeProgram.json"){initProgram = true;}
        webResponse = "File Uploaded " + filename;
      }else{
        error = true;
        webResponse = "File length mismatch";
      }
    }
    if( error ){
      revertBackFile(filename);
    }
    Serial.println(webResponse);
    AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", webResponse);
    response->addHeader("Access-Control-Allow-Origin","*");
    request->send(response);
  }

}

看来问题解决了

我已经设法用外部存储器中的字符替换了字符串缓冲区。它看起来稳定但需要更多测试。我会 post 解决方案,但如果有人有更好的方法,请随时在这里评论

谢谢。

char * uploadTemp;
inline boolean saveFileToTemp(String fileName){
  File f = LITTLEFS.open(fileName, "r");
  if (!f) {
    f.close();
    return false;
  }else{
    size_t fileSize = f.size();
    uploadTemp = (char*)ps_malloc(fileSize + 1);
    for (int i = 0; i < fileSize; i++){
      uploadTemp[i] = (char)f.read();
    }
    uploadTemp[fileSize] = '[=10=]';
  }
  f.close();
  return true;
}

inline boolean revertBackFile(String fileName){
  File g = LITTLEFS.open(fileName, "w");
  if (!g) {
    g.close();
    return false;
  }else{
    g.print(uploadTemp);
  }
  g.close();
  return true;
}

inline boolean matchFileSizes(String fileName,boolean isFileExists){
  boolean isCorrect = false;
  if(isFileExists){
    File writedFile = LITTLEFS.open(fileName, "w");
    if( writedFile.size() == sizeof(uploadTemp)){
      isCorrect = true;
    }else{
      isCorrect = false;
    }
    writedFile.close();
    return isCorrect;
  }else{
    return true;
  }
}

void handleFileUpload(AsyncWebServerRequest * request, String filename,size_t index, uint8_t *data, size_t len, bool final) {
  boolean isFileExists = false,error = false;
  String webResponse = "";
  int httpStatus = 200;

  // Start of the file upload
  if (!index) {
    // Make sure that there is a / char at the start of the string
    if (!filename.startsWith("/")){ filename = "/" + filename; }
    // Check if the file exists
    if (LITTLEFS.exists(filename)) {
      isFileExists = true;
      // Get the file contents for safety reasons
      // If it succeded we can create a new file in the palce
      if( saveFileToTemp(filename) ){
        uploadFile = LITTLEFS.open(filename, "w");
      }else{
        // If we can not save it abort the upload process.
        webResponse = "File NOT Uploaded " + filename;
        final = true;error = true;
      }
    }
  }
  // If we have no error at this point, we can start to copy the content to the file.
  if( !error ){
    for (size_t i = 0; i < len; i++) {
      uploadFile.write(data[i]);
    }
  }
  // If no more data we can start responding back to the client
  if (final) {
    uploadFile.close();
    // Check if we got any error before.

    if( !error && matchFileSizes(filename,isFileExists) ){
      // Copyed file is the same, upload success.
      if(filename == "/myHomeProgram.json"){initProgram = true;}
      webResponse = "File Uploaded " + filename;
    }else{
      webResponse = "File length mismatch";
      revertBackFile(filename);
      httpStatus = 500;
    }

    free(uploadTemp);
    AsyncWebServerResponse *response = request->beginResponse(httpStatus, "text/plain", webResponse);
    response->addHeader("Access-Control-Allow-Origin","*");
    request->send(response);

  }
}

编辑:

是啊,完全错了

我必须做以下事情:

  • 将我们要上传的文件保存到临时字符数组中。

  • 上传时将上传的文件变成临时文件。

  • 如果一切顺利,将临时文件的内容复制到目标文件中。

  • 如果失败,将保存的文件还原为原始文件并报告错误。

类似这样的东西(还在测试中):

char * prevFileTemp;

inline boolean saveFileToTemp(String fileName){
  File f = LITTLEFS.open(fileName, "r");
  if (!f) {
    f.close();
    return false;
  }else{
    size_t fileSize = f.size();
    prevFileTemp = (char*)ps_malloc(fileSize + 1);
    for (int i = 0; i < fileSize; i++){
      prevFileTemp[i] = (char)f.read();
    }
  }
  f.close();
  return true;
}

inline boolean revertBackFile(String fileName){
  if (LITTLEFS.exists(fileName)) {
    Serial.println("Reverting back the file");
    File g = LITTLEFS.open(fileName, "w");
    if (!g) {
      g.close();
      return false;
    }else{
      g.print(prevFileTemp);
    }
    g.close();
  }
  return true;
}


static const inline boolean copyContent(String fileName){
  File arrivedFile  = LITTLEFS.open(uploadTemp, "r");
  File newFile      = LITTLEFS.open(fileName, "w");
  // Check if we can open the files as intended.
  if( !arrivedFile || !newFile){
    revertBackFile(fileName);
    return false;
  }
  // Copy one file content to another.
  for (size_t i = 0; i < arrivedFile.size(); i++) { newFile.write( (char)arrivedFile.read() ); }
  // Check the sizes, if no match, abort mission.
  if( newFile.size() != arrivedFile.size()){ return false; }
  
  arrivedFile.close();newFile.close();
  return true;
}

boolean isFileExists = false,uploadError = false,newFileArrived = false;
String webResponse = "",newArrivalFileName = "";
int httpStatus = 200;

inline void resetVariables(){
  isFileExists  = false;
  uploadError   = false;
  webResponse   = "";
  httpStatus    = 200;
}


void handleFileUpload(AsyncWebServerRequest * request, String filename,size_t index, uint8_t *data, size_t len, bool final) {
  // Start file upload process
  if (!index) {
    // Reset all the variables
    resetVariables();
    // Make sure that there is a '/' char at the start of the string
    if (!filename.startsWith("/")){ filename = "/" + filename; }
    // Open the temporary file for content copy if it is exist
    if (LITTLEFS.exists(filename)) {
      if( saveFileToTemp(filename) ){
        uploadFile = LITTLEFS.open(uploadTemp, "w");
      }else{
        // If we can not save it abort the upload process.
        webResponse = "File NOT Uploaded " + filename;
        final = true;uploadError = true;
      }
    }
  }
  // If we have no error at this point, we can start to copy the content to the temporary file.
  if( !uploadError ){
    for (size_t i = 0; i < len; i++) {
      uploadFile.write(data[i]);
    }
  }
  // If no more data we can start responding back to the client
  if (final) {
    if (!filename.startsWith("/")){ filename = "/" + filename; }
    uploadFile.close();
    if( !uploadError && copyContent(filename) ){
      webResponse = "File Uploaded " + filename;
    }else{
      webResponse = "File length mismatch";
      revertBackFile(filename);
      httpStatus = 500;
    }
    free(prevFileTemp);
    AsyncWebServerResponse *response = request->beginResponse(httpStatus, "text/plain", webResponse);
    response->addHeader("Access-Control-Allow-Origin","*");
    request->send(response);
  }

}