ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
Mnt - mounted storage over a real filesystem

Layer: L7 Application ยท Build flags: PROTOCORE_ENABLE_MNT

What this example teaches

Storage in ProtoCore is two pieces that do two jobs. The mount (protocore_mnt_*) says what is behind the filesystem - a RAM pool in host tests, real flash on the device. The accessor (protocore_fs_*) is how you use it, and it owns the mount root and the resolved path. Features target the accessor, and the application chooses the medium.

Here it is mounted on LittleFS so writes persist across reboots; mounting the RAM backend instead changes nothing in the endpoints.

Mount a backend once, set the root once:

LittleFS.begin(true);
protocore_mnt_mount(protocore_mnt_fs(&LittleFS)); // real flash...
// protocore_mnt_mount(protocore_mnt_ram()); // ...or pure RAM - endpoints identical
protocore_fs_begin("/"); // what every name below is resolved against
void protocore_mnt_mount(uint8_t *work)

The file operations are backend-agnostic:

protocore_fs_write_file(name, data, strlen(data)); // create / overwrite
long n = protocore_fs_read_file(name, buf, cap); // n < 0 if absent
long sz = protocore_fs_size(name); // -1 if absent
protocore_fs_remove(name);

Notice what the handlers do not do: they never build a path. They pass the client's ?name= straight through. The accessor joins it onto the root and refuses any .. before storage is touched, so ?name=../../secret is rejected without this sketch containing a line about it. That is the reason path handling lives in one place instead of in every protocol server - there is one guard to get right, and one buffer, rather than one of each per caller.

Build and run

pio ci --board=esp32dev --project-option="framework=arduino" \
--project-option="build_flags=-DPROTOCORE_ENABLE_MNT=1" \
--lib="." examples/L7-Application/Mnt/Mnt.ino
curl "http://<ip>/save?name=greeting&data=hello" # store /greeting
curl "http://<ip>/load?name=greeting" # hello
curl "http://<ip>/size?name=greeting" # 5
curl "http://<ip>/rm?name=greeting" # delete
curl "http://<ip>/load?name=../../secret" # refused by the accessor

Annotated source

The complete sketch (Mnt.ino), reproduced verbatim with added explanatory comments:

// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
// SPDX-License-Identifier: AGPL-3.0-or-later
#define PROTOCORE_ENABLE_MNT 1
#include "protocore.h"
#include "test/core_setup/hal/esp/esp_mnt_fs.h" // the Arduino FS backend lives in the board layer
#include <LittleFS.h>
static const char *SSID = "YOUR_SSID";
static const char *PASSWORD = "YOUR_PASSWORD";
PC server;
void setup()
{
Serial.begin(115200);
Physical.wifi->init(SSID, PASSWORD);
while (!Physical.wifi->ready())
delay(250);
uint32_t ip = Physical.link->egress_ip(); // library egress IP (network byte order), no Arduino WiFi
Serial.printf("IP: %u.%u.%u.%u\n", (unsigned)(ip & 0xFF), (unsigned)((ip >> 8) & 0xFF),
(unsigned)((ip >> 16) & 0xFF), (unsigned)((ip >> 24) & 0xFF));
LittleFS.begin(true); // format on first use
protocore_mnt_mount(protocore_mnt_fs(&LittleFS));
protocore_fs_begin("/"); // every name below is resolved against this root
server.on("/save", HttpMethod::HTTP_GET, [](uint8_t id, HttpReq *req) {
const char *name = http_get_query(req, "name");
const char *data = http_get_query(req, "data");
if (!name || !*name || !data)
{
server.send(id, 400, "application/json", "{\"error\":\"name+data\"}");
return;
}
// The raw query value goes straight in - resolution and the `..` guard are the accessor's.
bool ok = protocore_fs_write_file(name, data, strlen(data));
server.send(id, ok ? 200 : 500, "application/json", ok ? "{\"ok\":true}" : "{\"ok\":false}");
});
server.on("/load", HttpMethod::HTTP_GET, [](uint8_t id, HttpReq *req) {
const char *name = http_get_query(req, "name");
if (!name || !*name)
{
server.send(id, 400, "text/plain", "name?");
return;
}
char buf[512];
long n = protocore_fs_read_file(name, buf, sizeof(buf) - 1);
if (n < 0) // absent, too big for buf, or a refused path - all one fail-closed answer
{
server.send(id, 404, "text/plain", "not found");
return;
}
buf[n] = '\0';
server.send(id, 200, "text/plain", buf);
});
server.on("/size", HttpMethod::HTTP_GET, [](uint8_t id, HttpReq *req) {
const char *name = http_get_query(req, "name");
long n = (name && *name) ? protocore_fs_size(name) : -1;
char b[24];
snprintf(b, sizeof(b), "%ld", n);
server.send(id, 200, "text/plain", b);
});
server.on("/rm", HttpMethod::HTTP_GET, [](uint8_t id, HttpReq *req) {
const char *name = http_get_query(req, "name");
bool ok = (name && *name) && protocore_fs_remove(name);
server.send(id, ok ? 200 : 404, "application/json", ok ? "{\"ok\":true}" : "{\"ok\":false}");
});
server.begin(80);
}
void loop()
{
server.handle();
}
The filesystem accessor: it owns the mount root, it owns the resolved path, and it is the only way a ...
@ HTTP_GET
Safe, idempotent read.
Definition http.h:24
Layer 1 (Physical) - link bring-up, the interface registry, and live egress reporting.