Storage API
File I/O and browser operations
The storage API provides file I/O for reading and writing files on the SD card.
Path Format
- Paths start with "/" - but that root is the tapp's OWN folder, not the card's. Every write goes to
.data/<appname>/and cannot leave it. - Example:
"/data.bin"is the card's.data/<appname>/data.bin - A leading "/" is optional:
"/save.pb"and"save.pb"are the same file - A write creates the folders it needs; storage_mkdir() is only for an empty folder.
- Maximum path length: about 240 characters (255 minus the tapp's folder prefix). An over-long path is REFUSED, never silently truncated.
Reads
A read (storage_read_file, storage_file_exists, storage_file_size, storage_list_dir) looks in the tapp's folder first and falls back to the path as given if nothing is there. This allows loading a file the user picked with browser_open, or a ROM/sample/WAV located anywhere on the card. For a folder it is all-or-nothing: once the tapp's folder has a directory of that name, storage_list_dir lists only that one, never the card's - so a tapp must not create a folder named like a card folder the user is expected to fill.
Writes have no such fallback: storage_write_file, storage_append_file, storage_delete_file and storage_mkdir always act inside the tapp's folder. A tapp cannot modify or delete anything belonging to the user or to the firmware.
Safe writes
storage_write_file() writes ATOMICALLY and wraps the data in a 16-byte integrity envelope (magic + length + CRC32): the write goes through "<path>.tmp" and the previous version is kept as "<path>.bak" - those siblings must be left untouched, they provide crash recovery. storage_read_file() and storage_file_size() strip the envelope, so a write/read round-trip returns exactly the bytes written, and fall back to the .bak copy if the file is damaged. Foreign/raw files (assets, ROMs, files not written with storage_write_file()) read back raw, partial reads allowed.
Open file browser to select a file
| const char* | start_path | Starting directory path (e.g., "/" for root) |
| const char* | file_extension | File extension filter (e.g., ".gb", ".txt") Set to NULL to show all files |
| browser_callback_t | callback | Callback function called when file is selected |
The browser will filter files by the given extension and call the callback when the user selects a file. The callback receives the full file path.
Returns — true if browser launched successfully
bool on_file_selected(const char* path) {
// Load the file
storage_read_file(path, buffer, sizeof(buffer));
return true;
}
browser_open("/", ".txt", on_file_selected);Read entire file into buffer
| const char* | path | File path (max 255 chars) |
| void* | buffer | Output buffer |
| size_t | buffer_size | Size of output buffer |
For a file written by storage_write_file() this returns the PAYLOAD (see Safe writes); any other file returns its raw bytes, truncated to buffer_size.
Returns — Number of bytes read, or 0 on error
uint8_t data[1024];
uint32_t bytes = storage_read_file("/config.bin", data, sizeof(data));
if (bytes > 0) {
// Process data
}Write entire file from buffer (overwrites existing)
| const char* | path | File path (max 255 chars) |
| const void* | buffer | Data to write |
| size_t | size | Number of bytes to write (must be > 0) |
Atomic and integrity-enveloped (see Safe writes): read it back with storage_read_file() - a raw reader would see the 16-byte envelope prepended.
Returns — Number of bytes written, or 0 on error
Append data to existing file
| const char* | path | File path (max 255 chars) |
| const void* | buffer | Data to append |
| size_t | size | Number of bytes to append |
RAW append - do NOT mix with storage_write_file() on the same path: appending to an enveloped file breaks its length/CRC, and reads of it degrade to raw (envelope bytes included). Use one API per file.
Returns — Number of bytes written, or 0 on error
Delete a file
| const char* | path | File path (max 255 chars) |
Deletes only <path> itself - a "<path>.bak" sibling from storage_write_file is left behind (harmless for reads; delete it as well to leave no trace).
Returns — true on success, false if file doesn't exist or error
Create directory (and parent directories if needed)
| const char* | path | Directory path (max 255 chars) |
Returns — true on success (also returns true if directory already exists)
Check if file exists
| const char* | path | File path (max 255 chars) |
Returns — true if file exists
Get file size in bytes
| const char* | path | File path (max 255 chars) |
For a file written by storage_write_file() this is the PAYLOAD size - i.e. exactly what storage_read_file() will return.
Returns — File size in bytes, or 0 if not found
List the files in a folder
| const char* | path | Folder path (max 255 chars) |
| const char* | ext | Extension filter, e.g. ".wav"; NULL lists every file |
| char* | names | Output buffer, written as `max` slots of `stride` bytes |
| size_t | stride | Bytes per slot - name `i` is at `names + i * stride` |
| uint32_t | max | Maximum number of names to write |
Returns — How many names were written
char names[8][32];
const uint32_t n = storage_list_dir("/kits", ".wav", &names[0][0], 32, 8);
for (uint32_t i = 0; i < n; i++) { ... } // names[i] is a filenameNote — *The order is the filesystem's**, which on FAT is directory-entry order - arbitrary, and it changes as files are created and deleted. If anything is indexed by position (a saved preset pointing at "kit 3", for example), SORT the names first, or that preset will point elsewhere after a copy.
Note — *The tapp's own `.bak` files are already excluded.** storage_write_file() leaves `<path>.bak` and `<path>.tmp` beside what it writes; `ext` matches the END of the name, so ".wav" does not match "kit.wav.bak". Passing NULL for `ext` turns that off and they are listed.
Note — Directories, hidden files and dotfiles never appear. Blocks on the SD card, so treat it like storage_read_file(): init() or tick(), never process().