Files
slate32/src/host/providers_fs.cpp
T
evan 45abf8a767 refactor: run apps on the shared esp32-lua-api runtime
Replaces the firmware's own lua_State, bindings, module loader, node tree
and navigation history with lib/esp32-lua-api. What is left in src/host is
the hardware behind the provider interfaces plus the chrome the firmware
owns: the viewport, the status bar and touch polling.

src/lua became src/host because src is on the include path, so a directory
named lua shadowed the library's <lua/providers.h> and #pragma once then
silently skipped it.
2026-08-03 18:12:33 -04:00

194 lines
6.0 KiB
C++

#include <SD.h>
#include "providers.h"
namespace slate {
namespace {
using esp32lua::Status;
// Streamed rather than slurped: holding a whole module as one buffer needs that many bytes
// contiguous, and once WiFi is up the largest free block is far below the free heap.
class SdReader : public esp32lua::FileReader {
public:
explicit SdReader(File file) : file(file) {}
~SdReader() override {
if (file) file.close();
}
int32_t read(char* out, int32_t maxBytes) override {
return file.read(reinterpret_cast<uint8_t*>(out), maxBytes);
}
private:
File file;
};
Status listEntries(const std::string& path, bool directories, std::vector<std::string>& names) {
File dir = SD.open(path.c_str());
if (!dir || !dir.isDirectory()) {
if (dir) dir.close();
return Status::failure("not a directory");
}
for (File entry = dir.openNextFile(); entry; entry = dir.openNextFile()) {
if (entry.isDirectory() == directories && entry.name()[0] != '.') names.push_back(entry.name());
entry.close();
}
dir.close();
std::sort(names.begin(), names.end());
return Status::success();
}
bool removeRecursive(const String& path) {
File entry = SD.open(path);
if (!entry) return false;
if (!entry.isDirectory()) {
entry.close();
return SD.remove(path);
}
for (File child = entry.openNextFile(); child; child = entry.openNextFile()) {
const String childPath = path + "/" + child.name();
const bool isDirectory = child.isDirectory();
child.close();
if (!(isDirectory ? removeRecursive(childPath) : SD.remove(childPath))) {
entry.close();
return false;
}
}
entry.close();
return SD.rmdir(path);
}
} // namespace
esp32lua::FileReader* Fs::openRead(const std::string& path) {
// Probed first: a missed SD.open logs a VFS error, and the module searcher misses by design.
if (!SD.exists(path.c_str())) return nullptr;
File file = SD.open(path.c_str());
if (!file || file.isDirectory()) {
if (file) file.close();
return nullptr;
}
return new SdReader(file);
}
bool Fs::exists(const std::string& path) const { return SD.exists(path.c_str()); }
Status Fs::fileSize(const std::string& path, int32_t& size) const {
File file = SD.open(path.c_str());
if (!file || file.isDirectory()) {
if (file) file.close();
return Status::failure("not a file");
}
size = static_cast<int32_t>(file.size());
file.close();
return Status::success();
}
Status Fs::listDirs(const std::string& path, std::vector<std::string>& names) const {
return listEntries(path, true, names);
}
Status Fs::listFiles(const std::string& path, std::vector<std::string>& names) const {
return listEntries(path, false, names);
}
Status Fs::mkdir(const std::string& path) {
if (SD.exists(path.c_str())) return Status::success();
return SD.mkdir(path.c_str()) ? Status::success() : Status::failure("cannot create directory");
}
Status Fs::readFile(const std::string& path, int32_t maxBytes, std::string& content) const {
File file = SD.open(path.c_str());
if (!file || file.isDirectory()) {
if (file) file.close();
return Status::failure("not a file");
}
const size_t size = file.size();
if (size > static_cast<size_t>(maxBytes)) {
file.close();
return Status::failure("file exceeds maxBytes");
}
content.resize(size);
const size_t read = size ? file.read(reinterpret_cast<uint8_t*>(&content[0]), size) : 0;
file.close();
if (read != size) return Status::failure("short read");
return Status::success();
}
Status Fs::readLineAt(const std::string& path, int32_t offset, int32_t maxBytes, bool& found, std::string& line,
int32_t& nextOffset) const {
File file = SD.open(path.c_str());
if (!file || file.isDirectory()) {
if (file) file.close();
return Status::failure("not a file");
}
if (!file.seek(offset)) {
file.close();
return Status::failure("cannot seek");
}
if (offset > 0) {
// A mid-line offset advances to the next line, so a caller can resume from any byte.
while (file.available() && file.peek() != '\n') file.read();
if (file.available()) file.read();
}
if (!file.available()) {
file.close();
found = false;
return Status::success();
}
line.clear();
while (file.available()) {
const int c = file.read();
if (c == '\n') break;
if (c != '\r') line.push_back(static_cast<char>(c));
if (static_cast<int32_t>(line.size()) > maxBytes) {
file.close();
return Status::failure("line exceeds maxBytes");
}
}
nextOffset = static_cast<int32_t>(file.position());
file.close();
found = true;
return Status::success();
}
Status Fs::remove(const std::string& path) {
return SD.remove(path.c_str()) ? Status::success() : Status::failure("cannot remove");
}
Status Fs::removeTree(const std::string& path) {
return removeRecursive(String(path.c_str())) ? Status::success() : Status::failure("cannot remove tree");
}
Status Fs::rename(const std::string& source, const std::string& destination) {
if (SD.exists(destination.c_str())) return Status::failure("destination exists");
return SD.rename(source.c_str(), destination.c_str()) ? Status::success() : Status::failure("cannot rename");
}
// Written to a temporary first: a half-written file that replaced a good one is worse than a
// failed write that left the old contents alone.
Status Fs::writeFile(const std::string& path, const std::string& content) {
const String target(path.c_str());
const String temporary = target + ".tmp";
SD.remove(temporary);
File file = SD.open(temporary, FILE_WRITE);
if (!file) return Status::failure("cannot open for writing");
const size_t written = content.empty() ? 0 : file.write(reinterpret_cast<const uint8_t*>(content.data()),
content.size());
file.close();
if (written != content.size()) {
SD.remove(temporary);
return Status::failure("short write");
}
SD.remove(target);
if (!SD.rename(temporary, target)) {
SD.remove(temporary);
return Status::failure("cannot replace destination");
}
return Status::success();
}
} // namespace slate