diff --git a/box3_voice/main/scenario_server.c b/box3_voice/main/scenario_server.c index f476f81..5bc2d2f 100644 --- a/box3_voice/main/scenario_server.c +++ b/box3_voice/main/scenario_server.c @@ -22,6 +22,9 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" +/* BSP SD card support (bsp_sdcard_mount, BSP_SD_MOUNT_POINT) */ +#include "bsp/esp-bsp.h" + #define TAG "scenario_srv" #define MAX_SCENARIO_BYTES (64 * 1024) @@ -216,6 +219,100 @@ static esp_err_t handle_scenario_post(httpd_req_t *req) { return send_json(req, "200 OK", buf); } +// ---------- POST /game/file ---------- +// +// Write binary assets directly to the SD card over HTTP. +// Query param: ?path=sd/ (only "sd/" prefix accepted) +// Route: sd/<…> → /sdcard/<…> (mkdir -p, up to 8 MiB, anti-traversal). +// Returns 503 if SD is not mounted, 403 on bad path, 200 JSON on success. + +#define MAX_FILE_BYTES (8L * 1024L * 1024L) + +static bool s_sd_mounted = false; + +static esp_err_t ensure_sd_mounted(void) { + if (s_sd_mounted) return ESP_OK; + esp_err_t ret = bsp_sdcard_mount(); + if (ret == ESP_OK || ret == ESP_ERR_INVALID_STATE /* already mounted */) { + s_sd_mounted = true; + ESP_LOGI(TAG, "file: SD mounted at %s", BSP_SD_MOUNT_POINT); + return ESP_OK; + } + ESP_LOGW(TAG, "file: SD mount failed: %s", esp_err_to_name(ret)); + return ret; +} + +static esp_err_t handle_file_post(httpd_req_t *req) { + char query[160], path_param[96]; + if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK || + httpd_query_key_value(query, "path", path_param, + sizeof(path_param)) != ESP_OK) { + return send_error(req, "400 Bad Request", "missing path param"); + } + + // Only accept sd/<…> prefix; reject traversal and bare directory paths. + if (strncmp(path_param, "sd/", 3) != 0 || + strstr(path_param, "..") || + path_param[strlen(path_param) - 1] == '/') { + return send_error(req, "403 Forbidden", "path must start with sd/ and name a file"); + } + + if (req->content_len <= 0 || (long)req->content_len > MAX_FILE_BYTES) { + return send_error(req, "400 Bad Request", "file too large or empty"); + } + + if (ensure_sd_mounted() != ESP_OK) { + return send_error(req, "503 Service Unavailable", "sd_not_mounted"); + } + + // Build absolute destination: /sdcard/<…> (strip the "sd/" prefix). + char full[192]; + snprintf(full, sizeof(full), "%s/%s", BSP_SD_MOUNT_POINT, path_param + 3); + + // mkdir -p for all intermediate directories. + const size_t root_len = strlen(BSP_SD_MOUNT_POINT) + 1; // e.g. strlen("/sdcard/") + for (char *p = full + root_len; *p; p++) { + if (*p == '/') { + *p = '\0'; + mkdir(full, 0775); // EEXIST is fine + *p = '/'; + } + } + + FILE *f = fopen(full, "wb"); + if (!f) { + ESP_LOGE(TAG, "file: fopen %s failed (errno=%d)", full, errno); + return send_error(req, "500 Internal Server Error", "open failed"); + } + + char buf[1024]; + int remaining = (int)req->content_len; + while (remaining > 0) { + const int want = remaining < (int)sizeof(buf) ? remaining : (int)sizeof(buf); + int got = httpd_req_recv(req, buf, want); + if (got <= 0) { + if (got == HTTPD_SOCK_ERR_TIMEOUT) continue; + fclose(f); + unlink(full); + return send_error(req, "400 Bad Request", "recv failed"); + } + if (fwrite(buf, 1, (size_t)got, f) != (size_t)got) { + fclose(f); + unlink(full); + return send_error(req, "500 Internal Server Error", "write failed"); + } + remaining -= got; + } + fclose(f); + + char resp[192]; + snprintf(resp, sizeof(resp), + "{\"status\":\"ok\",\"path\":\"%s\",\"bytes\":%d}", + path_param, (int)req->content_len); + ESP_LOGI(TAG, "file: stored %s (%d B)", full, (int)req->content_len); + return send_json(req, "200 OK", resp); +} + // ---------- public init ---------- httpd_handle_t scenario_server_handle(void) { @@ -229,7 +326,7 @@ esp_err_t scenario_server_start(void) { } httpd_config_t cfg = HTTPD_DEFAULT_CONFIG(); cfg.server_port = 80; - cfg.max_uri_handlers = 8; + cfg.max_uri_handlers = 10; cfg.stack_size = 8192; esp_err_t err = httpd_start(&s_server, &cfg); @@ -246,9 +343,14 @@ esp_err_t scenario_server_start(void) { .uri = "/game/scenario", .method = HTTP_POST, .handler = handle_scenario_post, .user_ctx = NULL, }; + static const httpd_uri_t uri_file = { + .uri = "/game/file", .method = HTTP_POST, + .handler = handle_file_post, .user_ctx = NULL, + }; httpd_register_uri_handler(s_server, &uri_healthz); httpd_register_uri_handler(s_server, &uri_scenario); + httpd_register_uri_handler(s_server, &uri_file); - ESP_LOGI(TAG, "scenario server up on :80 (GET /healthz, POST /game/scenario)"); + ESP_LOGI(TAG, "scenario server up on :80 (GET /healthz, POST /game/scenario, POST /game/file)"); return ESP_OK; }