45abf8a767
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.
194 lines
6.0 KiB
C++
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
|