HTTP REST API 経由で ESP32 LittleFS にファイルをアップロードする方法

この記事は、リクエストボディを LittleFS 内のファイルに書き込む PUT エンドポイントを ESP32(ESP-IDF esp_http_server)に実装する方法と、curl でファイルをアップロードする方法を示す。

エンドポイント

example.txt
PUT /api/files/put?path=<relative/path>

リクエストボディは生のファイル内容。ハンドラはパスをサニタイズ(.. を拒否、先頭スラッシュとクエリ文字列を除去)し、必要に応じて親ディレクトリを作成し、ボディを 1 KiB チャンクでフラッシュにストリーミングする。

ファームウェアハンドラ(ESP-IDF)

HTTPHandlersFiles.cpp
esp_err_t handlePutFile(httpd_req_t *req) {
    // PUT /api/files/put?path=<relative/path> を期待
    QueryURLParser parser(req);
    if (!parser.HasParameter("path")) {
        httpd_resp_set_status(req, "400 Bad Request");
        httpd_resp_sendstr(req, "{\"error\": \"Missing path parameter\"}");
        return ESP_OK;
    }

    std::string rel = parser.GetParameter("path");
    if (!sanitize_path(rel, rel) || rel.empty()) {
        httpd_resp_set_status(req, "400 Bad Request");
        httpd_resp_sendstr(req, "{\"error\": \"Invalid path\"}");
        return ESP_OK;
    }

    std::string fullpath = std::string(LittleFSManager::mountPoint()) + "/" + rel;
    ensure_parent_dirs(fullpath);

    FILE* f = fopen(fullpath.c_str(), "wb");
    if (!f) {
        httpd_resp_set_status(req, "500 Internal Server Error");
        httpd_resp_sendstr(req, "{\"error\": \"Failed to open file for writing\"}");
        return ESP_OK;
    }

    const int len = req->content_len;
    int received = 0;
    char buffer[1024];
    while (received < len) {
        int to_read = std::min((int)sizeof(buffer), len - received);
        int ret = httpd_req_recv(req, buffer, to_read);
        if (ret <= 0) {
            fclose(f);
            httpd_resp_set_status(req, "500 Internal Server Error");
            httpd_resp_sendstr(req, "{\"error\": \"Failed to read request body\"}");
            return ESP_OK;
        }
        fwrite(buffer, 1, ret, f);
        received += ret;
    }
    fclose(f);

    set_content_type_with_charset(req, "application/json");
    httpd_resp_sendstr(req, "{\"status\":\"success\"}");
    return ESP_OK;
}

HTTP サーバに登録:

HTTPServerWrapper.cpp
static const httpd_uri_t putFileHandler = {
    .uri       = "/api/files/put",
    .method    = HTTP_PUT,
    .handler   = handlePutFile,
    .user_ctx  = NULL
};
httpd_register_uri_handler(server, &putFileHandler);

curl でファイルをアップロード

upload.sh
curl -X PUT --data-binary @index.html \
  "http://10.56.11.162/api/files/put?path=index.html"

応答:

response.json
{"status":"success","path":"index.html"}

重要: 固定長ボディを送ること。HTTP クライアントが Transfer-Encoding: chunked を使う場合(例:ジェネレータボディを持つ Python requests)、esp_http_server は req->content_len が正しい Content-Length ヘッダに依存するため 400 Bad Request でリクエストを拒否する可能性がある。curl --data-binary @file は Content-Length を正しく送る。

重要:パスをサニタイズ

?path= を決してファイルシステムパスに直接連結しない——さもないと path=../../etc/passwd 式のトラバーサルで LittleFS マウントから脱出される。最小限のサニタイザ:

HTTPHandlersFiles.cpp
bool sanitize_path(const std::string &in, std::string &out) {
    std::string dec = url_decode(in);
    // クエリ文字列とフラグメントを除去
    size_t qpos = dec.find('?');
    if (qpos != std::string::npos) dec = dec.substr(0, qpos);
    size_t hpos = dec.find('#');
    if (hpos != std::string::npos) dec = dec.substr(0, hpos);
    // 先頭スラッシュを除去 -> 常にマウントポイントに対して相対
    size_t start = 0;
    while (start < dec.size() && dec[start] == '/') start++;
    out = dec.substr(start);
    if (out.find("..") != std::string::npos) return false;
    return true;
}

アップロードを検証

アップロード後、CRC32 エンドポイントで整合性を検証——How to verify ESP32 LittleFS file uploads over HTTP using a CRC32 endpoint を参照。

関連記事


同じカテゴリの類似記事をチェック: ESP32 C/C++