feat(game): POST /game/file provisions apps
This commit is contained in:
@@ -872,6 +872,75 @@ void game_endpoint_get_puzzle_status(char *step_id, size_t step_cap,
|
||||
}
|
||||
}
|
||||
|
||||
// ─── POST /game/file?path=apps/<id>/<file> — provision shell apps ────────────
|
||||
// Raw body → file under /littlefs/apps/ ONLY (whitelist + no traversal).
|
||||
// Lets the GM push icon.png / step.txt for the display shell's dynamic tiles
|
||||
// without reflashing a littlefs image.
|
||||
#define GAME_ENDPOINT_MAX_FILE_BYTES (256 * 1024)
|
||||
|
||||
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");
|
||||
}
|
||||
if (strncmp(path_param, "apps/", 5) != 0 || strstr(path_param, "..") ||
|
||||
path_param[strlen(path_param) - 1] == '/') {
|
||||
return send_error(req, "403 Forbidden", "path must be under apps/");
|
||||
}
|
||||
if (req->content_len <= 0 ||
|
||||
req->content_len > GAME_ENDPOINT_MAX_FILE_BYTES) {
|
||||
return send_error(req, "400 Bad Request", "size 1..262144 bytes");
|
||||
}
|
||||
if (mount_storage_lazy() != ESP_OK) {
|
||||
return send_error(req, "503 Service Unavailable", "storage_unavailable");
|
||||
}
|
||||
|
||||
char full[160];
|
||||
snprintf(full, sizeof(full), "/littlefs/%s", path_param);
|
||||
// mkdir -p for intermediate directories.
|
||||
for (char *p = full + strlen("/littlefs/"); *p; p++) {
|
||||
if (*p == '/') {
|
||||
*p = '\0';
|
||||
mkdir(full, 0775); // EEXIST is fine
|
||||
*p = '/';
|
||||
}
|
||||
}
|
||||
|
||||
FILE *f = fopen(full, "wb");
|
||||
if (!f) {
|
||||
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);
|
||||
}
|
||||
|
||||
void game_endpoint_get_scene(scene_binding_t *out) {
|
||||
if (!out) return;
|
||||
// Lock-free snapshot, same class as get_puzzle_status: written on the
|
||||
@@ -937,6 +1006,12 @@ esp_err_t game_endpoint_init(httpd_handle_t server) {
|
||||
.handler = handle_puzzle_state_get,
|
||||
.user_ctx = NULL,
|
||||
};
|
||||
static const httpd_uri_t uri_file_post = {
|
||||
.uri = "/game/file",
|
||||
.method = HTTP_POST,
|
||||
.handler = handle_file_post,
|
||||
.user_ctx = NULL,
|
||||
};
|
||||
|
||||
// Bring up the ESP-NOW mesh transport. The master is primarily a sender
|
||||
// (the relay handler) but we also register the apply adapter so a peer
|
||||
@@ -990,6 +1065,10 @@ esp_err_t game_endpoint_init(httpd_handle_t server) {
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "register POST /game/step: %s", esp_err_to_name(err));
|
||||
}
|
||||
err = httpd_register_uri_handler(server, &uri_file_post);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "register /game/file: %s", esp_err_to_name(err));
|
||||
}
|
||||
err = httpd_register_uri_handler(server, &uri_puzzle_state_get);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "register GET /game/puzzle_state: %s", esp_err_to_name(err));
|
||||
|
||||
@@ -310,8 +310,9 @@ esp_err_t ota_server_init(void) {
|
||||
// HTTP server config
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.server_port = OTA_SERVER_PORT;
|
||||
config.max_uri_handlers = 18; // ota + voice_hook + game (incl. /game/step,
|
||||
// /game/puzzle_state, relay) + headroom
|
||||
config.max_uri_handlers = 20; // ota + voice_hook + game (incl. /game/step,
|
||||
// /game/puzzle_state, /game/file, relay)
|
||||
// + headroom
|
||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||
config.stack_size = 8192;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user