この記事は、リクエストボディを 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 を参照。
関連記事
この記事が役に立った場合は、TechOverflow での新しい記事の調査と公開を支援するため、「Buy Me a Coffee」でのご支援、または PayPal 経由でのご寄付をご検討ください