#include #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(out), maxBytes); } private: File file; }; Status listEntries(const std::string& path, bool directories, std::vector& 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(file.size()); file.close(); return Status::success(); } Status Fs::listDirs(const std::string& path, std::vector& names) const { return listEntries(path, true, names); } Status Fs::listFiles(const std::string& path, std::vector& 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(maxBytes)) { file.close(); return Status::failure("file exceeds maxBytes"); } content.resize(size); const size_t read = size ? file.read(reinterpret_cast(&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(c)); if (static_cast(line.size()) > maxBytes) { file.close(); return Status::failure("line exceeds maxBytes"); } } nextOffset = static_cast(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(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