You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为ESP32简易文件服务器添加文件列表、删除及下载功能求助

ESP32文件服务器扩展需求:添加文件列表、删除及下载功能

现有基于ESP32 + SPIFFS + AsyncWebServer的代码仅支持文件上传,需要扩展文件列表展示、文件删除、文件下载功能,尝试过GitHub找方案但未找到可行方法。

现有ESP32代码

#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SPI.h>
#include <SPIFFS.h>

void setup() {

  Serial.begin(115200);
  
  if(!SPIFFS.begin()){
          Serial.println("An Error has occurred while mounting SPIFFS");
  }

  start_access_point();
  start_web_server();
}

void loop()
{
  
}

const char* ap_ssid = "ESP32";
const char* ap_pwd = "12345678";

void start_access_point(){
  WiFi.softAP(ap_ssid, ap_pwd);
  IPAddress myIP = WiFi.softAPIP();  
  Serial.println("Access point started");
  Serial.print("AP IP address: ");
  Serial.println(myIP);
}

AsyncWebServer server(80);

const char* PARAM_SSID = "ssid";
const char* PARAM_PWD = "pwd";

void start_web_server(){
  
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
      request->send(SPIFFS, "/index.html", "text/html");
  });


  server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request){
    if(request->hasArg("file")){
      String fileName = request->arg("file");
      File f = SPIFFS.open("/data/fileName", "w");
      if(!f){
        request->send(500, "text/plain", "Could not open file for writing");
        return;
      }
      std::string fileData = std::string(request->arg("file").c_str());
      f.write((uint8_t*)fileData.c_str(), fileData.length());
      f.close();
      request->send(200, "text/plain", "File uploaded successfully");
    }
    else{
      request->send(500, "text/plain", "No file data found in request");
    }
  });
  server.begin();
}

对应HTML代码

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <style>.app-container{
    border-radius: 5px;
    background-color: #f2f2f2;
    padding: 20px;
}

h3{
    text-align: center;
    color: #5a5c69 !important;
    font-family: Arial, Helvetica, sans-serif;
}

/* Style inputs, select elements and textareas */
input[type=text], select, textarea{
    width: 100%;
    padding: 12px;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box;
    resize: vertical;
  }

input[type=password], select, textarea{
    width: 100%;
    padding: 12px;
    border: 1px solid #ccc;
    border-radius: 4px;
    box-sizing: border-box;
    resize: vertical;
  }
  
  /* Style the label to display next to the inputs */
  label {
    padding: 12px 12px 12px 0;
    display: inline-block;
  }
  
  /* Style the submit button */
  input[type=submit] {
    background-color: #4CAF50;
    color: white;
    padding: 12px 20px;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    float: right;
  }
  
  /* Floating column for labels: 25% width */
.col-25 {
    float: left;
    width: 25%;
    margin-top: 6px;
  }
  
  /* Floating column for inputs: 75% width */
  .col-75 {
    float: left;
    width: 75%;
    margin-top: 6px;
  }
  
  /* Clear floats after the columns */
  .row:after {
    content: "";
    display: table;
    clear: both;
  }
  
  /* Responsive layout - when the screen is less than 600px wide, make the two columns stack on top of each other instead of next to each other */
  @media screen and (max-width: 600px) {
    .col-25, .col-75, input[type=submit] {
      width: 100%;
      margin-top: 0;
    }
  }
    </style>
    <title id="apptitle">Setup Wifi</title>
<head>
    <title>File Upload</title>
  </head>
  <body>
    <h1>File Upload</h1>
    <form method="post" action="/upload" enctype="multipart/form-data">
      <input type="file" name="file" />
      <input type="submit" value="Upload" />
    </form>
    <a href="/download/fileName">Download file</a>
  </body>
</html>

解决方案

一、修改ESP32代码(添加完整功能)

#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SPI.h>
#include <SPIFFS.h>

void setup() {
  Serial.begin(115200);
  
  // 挂载SPIFFS,失败时自动格式化
  if(!SPIFFS.begin(true)){
    Serial.println("An Error has occurred while mounting SPIFFS");
  }

  start_access_point();
  start_web_server();
}

void loop() {}

const char* ap_ssid = "ESP32";
const char* ap_pwd = "12345678";

void start_access_point(){
  WiFi.softAP(ap_ssid, ap_pwd);
  IPAddress myIP = WiFi.softAPIP();  
  Serial.println("Access point started");
  Serial.print("AP IP address: ");
  Serial.println(myIP);
}

AsyncWebServer server(80);

// 生成文件列表HTML片段
String getFileListHTML() {
  String html = "<ul>";
  File root = SPIFFS.open("/");
  File file = root.openNextFile();
  
  while (file) {
    if (!file.isDirectory()) {
      String fileName = file.name();
      // 跳过首页文件,避免误删
      if (fileName != "/index.html") {
        html += "<li>";
        html += fileName.substring(1); // 去掉开头的斜杠
        html += " <a href='/download?file=" + fileName + "' class='download-link'>下载</a>";
        html += " <a href='/delete?file=" + fileName + "' class='delete-link' onclick='return confirm(\"确定删除该文件?\")'>删除</a>";
        html += "</li>";
      }
    }
    file = root.openNextFile();
  }
  html += "</ul>";
  return html;
}

void start_web_server(){
  
  // 首页:动态注入文件列表
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    File indexFile = SPIFFS.open("/index.html", "r");
    if (indexFile) {
      String htmlContent = indexFile.readString();
      indexFile.close();
      // 替换占位符为实际文件列表
      htmlContent.replace("{{FILE_LIST}}", getFileListHTML());
      request->send(200, "text/html", htmlContent);
    } else {
      request->send(500, "text/plain", "Failed to load index page");
    }
  });

  // 文件上传:修复multipart/form-data处理逻辑
  server.on("/upload", HTTP_POST, 
    [](AsyncWebServerRequest *request){
      request->redirect("/"); // 上传完成后跳回首页
    },
    [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
      if (!index) {
        // 开始上传,创建文件
        request->_tempFile = SPIFFS.open("/" + filename, "w");
        if (!request->_tempFile) {
          Serial.println("Failed to open file for writing");
          request->send(500, "text/plain", "Upload failed: cannot open file");
          return;
        }
        Serial.printf("Upload started: %s\n", filename.c_str());
      }
      if (len) {
        // 写入文件数据
        request->_tempFile.write(data, len);
      }
      if (final) {
        // 上传完成,关闭文件
        request->_tempFile.close();
        Serial.printf("Upload finished: %s\n", filename.c_str());
      }
    }
  );

  // 文件下载
  server.on("/download", HTTP_GET, [](AsyncWebServerRequest *request){
    if (request->hasParam("file")) {
      String filePath = request->getParam("file")->value();
      if (SPIFFS.exists(filePath)) {
        // 以附件形式返回文件,触发浏览器下载
        request->send(SPIFFS, filePath, "application/octet-stream", true);
      } else {
        request->send(404, "text/plain", "File not found");
      }
    } else {
      request->send(400, "text/plain", "Missing file parameter");
    }
  });

  // 文件删除
  server.on("/delete", HTTP_GET, [](AsyncWebServerRequest *request){
    if (request->hasParam("file")) {
      String filePath = request->getParam("file")->value();
      // 禁止删除首页文件
      if (filePath == "/index.html") {
        request->send(403, "text/plain", "Cannot delete index.html");
        return;
      }
      if (SPIFFS.exists(filePath)) {
        if (SPIFFS.remove(filePath)) {
          request->send(200, "text/plain", "File deleted");
        } else {
          request->send(500, "text/plain", "Failed to delete file");
        }
      } else {
        request->send(404, "text/plain", "File not found");
      }
    } else {
      request->send(400, "text/plain", "Missing file parameter");
    }
    // 删除后跳回首页刷新列表
    request->redirect("/");
  });

  server.begin();
}

二、修改HTML代码(优化界面与交互)

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <style>
    .app-container{
      border-radius: 5px;
      background-color: #f2f2f2;
      padding: 20px;
      max-width: 800px;
      margin: 20px auto;
      font-family: Arial, Helvetica, sans-serif;
    }

    h1, h3{
      text-align: center;
      color: #5a5c69;
    }

    .upload-form {
      margin: 20px 0;
      text-align: center;
    }

    input[type=file], input[type=submit] {
      padding: 10px;
      margin: 5px;
      border-radius: 4px;
      border: 1px solid #ccc;
    }

    input[type=submit] {
      background-color: #4CAF50;
      color: white;
      border: none;
      cursor: pointer;
      transition: background-color 0.2s;
    }

    input[type=submit]:hover {
      background-color: #45a049;
    }

    .file-list {
      margin-top: 30px;
      padding: 15px;
      background-color: white;
      border-radius: 4px;
    }

    .file-list ul {
      list-style-type: none;
      padding: 0;
    }

    .file-list li {
      padding: 10px;
      border-bottom: 1px solid #eee;
      display: flex;
      align-items: center;
      justify-content: space-between;
    }

    .file-list a {
      text-decoration: none;
      padding: 6px 12px;
      border-radius: 4px;
      font-size: 14px;
      transition: opacity 0.2s;
    }

    .file-list a:hover {
      opacity: 0.8;
    }

    .download-link {
      background-color: #2196F3;
      color: white;
      margin-right: 8px;
    }

    .delete-link {
      background-color: #f44336;
      color: white;
    }
  </style>
  <title>ESP32文件服务器</title>
</head>
<body>
  <div class="app-container">
    <h1>ESP32文件服务器</h1>
    <form class="upload-form" method="post" action="/upload" enctype="multipart/form-data">
      <input type="file" name="file" required />
      <input type="submit" value="上传文件" />
    </form>

    <div class="file-list">
      <h3>已上传文件</h3>
      {{FILE_LIST}}
    </div>
  </div>
</body>
</html>

关键修改说明

  1. 修复上传逻辑:原代码错误使用request->arg("file")获取文件内容,改为AsyncWebServer标准的multipart上传回调处理,支持大文件分片上传
  2. 文件列表功能:新增getFileListHTML()遍历SPIFFS目录,生成带下载/删除链接的HTML片段,动态注入首页
  3. 文件下载功能:通过/download接口接收文件名参数,从SPIFFS读取文件并以附件形式返回
  4. 文件删除功能:通过/delete接口接收文件名参数,删除指定文件后自动跳回首页刷新列表
  5. 界面优化:调整HTML布局与样式,提升移动端适配性和交互体验

内容的提问来源于stack exchange,提问作者ParrotXray

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 20:30:41