#include "io/fs_handler.h" #include #include #include #include #include "esp_partition.h" #define TAG "LittleFSHandler" #define PARTITION_LABEL "storage" #define BLOCK_SIZE 512 // Match typical flash sector size // // FSGuard implementation // FSGuard::FSGuard(LittleFSHandler* fs_handler, const std::string& relative_path, const char* flags) : fs_handler_(fs_handler), file_(nullptr) { if (fs_handler_ != nullptr) { fs_handler_->open_file_(relative_path, flags, file_); } else { ESP_LOGE("FSGuard", "FSGuard initialized with null LittleFSHandler"); } } FSGuard::~FSGuard() { this->close(); } esp_err_t FSGuard::close() { if (file_ != nullptr && fs_handler_ != nullptr) { esp_err_t err = fs_handler_->close_file_(file_); file_ = nullptr; fs_handler_ = nullptr; if (err != ESP_OK) { ESP_LOGE("FSGuard", "Error closing file: %s", esp_err_to_name(err)); } return err; } return ESP_OK; } // // LittleFSHandler implementation // LittleFSHandler::LittleFSHandler() { this->fs_mutex_ = xSemaphoreCreateMutex(); if (this->fs_mutex_ == nullptr) { ESP_LOGE(TAG, "Failed to create filesystem mutex"); } } LittleFSHandler::~LittleFSHandler() { if (this->is_initialized_()) { esp_vfs_littlefs_unregister(PARTITION_LABEL); this->initialized_ = false; } if (this->fs_mutex_ != nullptr) { vSemaphoreDelete(this->fs_mutex_); this->fs_mutex_ = nullptr; } } esp_err_t LittleFSHandler::init(std::string base_path) { // default config esp_vfs_littlefs_conf_t config = {}; config.dont_mount = false; config.partition_label = PARTITION_LABEL; config.base_path = base_path.c_str(); config.format_if_mount_failed = true; // base_path_ = base_path; return init(config); } esp_err_t LittleFSHandler::init(const esp_vfs_littlefs_conf_t& config) { base_path_ = std::string(config.base_path); if (this->is_initialized_()) { ESP_LOGW(TAG, "LittleFS already initialized, skipping"); return ESP_OK; } esp_err_t err = esp_vfs_littlefs_register(&config); if (err != ESP_OK) { ESP_LOGE(TAG, "Failed to initialize LittleFS: %s", esp_err_to_name(err)); if (err == ESP_ERR_NOT_FOUND) { ESP_LOGE(TAG, "Listing all available partitions:"); esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); while (it != NULL) { const esp_partition_t* part = esp_partition_get(it); ESP_LOGE(TAG, " - Label: '%s', Type: 0x%02x, Subtype: 0x%02x, Address: 0x%08x, Size: 0x%08x", part->label, part->type, part->subtype, part->address, part->size); it = esp_partition_next(it); } esp_partition_iterator_release(it); } return ESP_ERR_INVALID_STATE; } this->initialized_ = true; return ESP_OK; } std::string LittleFSHandler::get_base_path() const { if (!this->is_initialized_()) { ESP_LOGE(TAG, "LittleFS is not initialized, cannot get base path"); return ""; } return base_path_; } std::string LittleFSHandler::get_full_path(const std::string& relative_path) const { if (!this->is_initialized_()) { ESP_LOGE(TAG, "LittleFS is not initialized, cannot get full path"); return ""; } return base_path_ + "/" + relative_path; } esp_err_t LittleFSHandler::write_file(const std::string& relative_path, const uint8_t* data, size_t size, size_t& out_bytes_written) { if (!this->is_initialized_()) { ESP_LOGE(TAG, "LittleFS is not initialized"); return ESP_ERR_INVALID_STATE; } if (data == nullptr) { ESP_LOGE(TAG, "Data pointer is null"); return ESP_ERR_INVALID_ARG; } SemaphoreGuard guard(this->fs_mutex_); if (guard.take() != pdTRUE) { ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); return ESP_ERR_TIMEOUT; } // Try to open with r+b first to preserve existing content for comparison FSGuard file_guard(this, relative_path, "r+b"); // If file doesn't exist, open with wb if (!file_guard.is_open()) { FSGuard new_file_guard(this, relative_path, "wb"); if (!new_file_guard.is_open()) { return ESP_ERR_NOT_FOUND; } return this->write_if_different_(new_file_guard.get_file(), data, size, out_bytes_written); } return this->write_if_different_(file_guard.get_file(), data, size, out_bytes_written); } // Caller is responsible for opening the file in appropriate mode // If the file doesn't exist, use write_file with "wb" mode // If the file exists, use "r+b" mode to read and write esp_err_t LittleFSHandler::write_file(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written) { return this->write_if_different_(file, data, size, out_bytes_written); } esp_err_t LittleFSHandler::append_file(const std::string& relative_path, const uint8_t* data, size_t size, size_t& out_bytes_written) { if (!this->is_initialized_()) { ESP_LOGE(TAG, "LittleFS is not initialized"); return ESP_ERR_INVALID_STATE; } if (data == nullptr) { ESP_LOGE(TAG, "Data pointer is null"); return ESP_ERR_INVALID_ARG; } SemaphoreGuard guard(this->fs_mutex_); if (guard.take() != pdTRUE) { ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); return ESP_ERR_TIMEOUT; } FSGuard file_guard(this, relative_path, "ab"); FILE* file = file_guard.get_file(); if (file == nullptr) { return ESP_ERR_NOT_FOUND; } return this->append_file(file, data, size, out_bytes_written); } esp_err_t LittleFSHandler::append_file(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written) { if (file == nullptr) { ESP_LOGE(TAG, "File pointer is null"); return ESP_ERR_INVALID_ARG; } if (data == nullptr) { ESP_LOGE(TAG, "Data pointer is null"); return ESP_ERR_INVALID_ARG; } if (fseek(file, 0, SEEK_END) != 0) { ESP_LOGE(TAG, "Failed to seek to end of file"); return ESP_ERR_INVALID_STATE; } // write data with POSIX size_t bytes_written = fwrite(data, 1, size, file); if (bytes_written != size) { ESP_LOGE(TAG, "Failed to write all data to file, expected %zu bytes, wrote %zu bytes", size, bytes_written); return ESP_ERR_NO_MEM; } if (fflush(file) != 0) { ESP_LOGE(TAG, "Failed to flush file"); return ESP_ERR_INVALID_STATE; } out_bytes_written = bytes_written; return ESP_OK; } esp_err_t LittleFSHandler::read_file(const std::string& relative_path, const size_t max_size, uint8_t* out_data, size_t& out_size) { if (!this->is_initialized_()) { ESP_LOGE(TAG, "LittleFS is not initialized"); return ESP_ERR_INVALID_STATE; } if (out_data == nullptr) { ESP_LOGE(TAG, "Output data pointer is null"); return ESP_ERR_INVALID_ARG; } SemaphoreGuard guard(this->fs_mutex_); if (guard.take() != pdTRUE) { ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); return ESP_ERR_TIMEOUT; } FSGuard file_guard(this, relative_path, "rb"); FILE* file = file_guard.get_file(); if (file == nullptr) { return ESP_ERR_NOT_FOUND; } return this->read_file(file, max_size, out_data, out_size); } esp_err_t LittleFSHandler::read_file(FILE* file, const size_t max_size, uint8_t* out_data, size_t& out_size) { if (file == nullptr) { ESP_LOGE(TAG, "File pointer is null"); return ESP_ERR_INVALID_ARG; } if (out_data == nullptr) { ESP_LOGE(TAG, "Output data pointer is null"); return ESP_ERR_INVALID_ARG; } size_t bytes_read = fread(out_data, 1, max_size, file); if (bytes_read == 0 && ferror(file)) { ESP_LOGE(TAG, "Failed to read from file"); return ESP_ERR_INVALID_STATE; } out_size = bytes_read; return ESP_OK; } esp_err_t LittleFSHandler::delete_file(const std::string& relative_path) { if (!this->is_initialized_()) { ESP_LOGE(TAG, "LittleFS is not initialized"); return ESP_ERR_INVALID_STATE; } SemaphoreGuard guard(this->fs_mutex_); if (guard.take() != pdTRUE) { ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); return ESP_ERR_TIMEOUT; } std::string full_path = this->get_full_path(relative_path); if (remove(full_path.c_str()) != 0) { ESP_LOGE(TAG, "Failed to delete file %s: %s", full_path.c_str(), strerror(errno)); return ESP_ERR_NOT_FOUND; } return ESP_OK; } bool LittleFSHandler::file_exists(const std::string& relative_path) { if (!this->is_initialized_()) { ESP_LOGE(TAG, "LittleFS is not initialized"); return false; } SemaphoreGuard guard(this->fs_mutex_); if (guard.take() != pdTRUE) { ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); return false; } std::string full_path = this->get_full_path(relative_path); struct stat st; return (stat(full_path.c_str(), &st) == 0 && S_ISREG(st.st_mode)); } esp_err_t LittleFSHandler::get_file_size(const std::string& relative_path, size_t& out_size) { if (!this->is_initialized_()) { ESP_LOGE(TAG, "LittleFS is not initialized"); return ESP_ERR_INVALID_STATE; } SemaphoreGuard guard(this->fs_mutex_); if (guard.take() != pdTRUE) { ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); return ESP_ERR_TIMEOUT; } std::string full_path = this->get_full_path(relative_path); struct stat st; if (stat(full_path.c_str(), &st) != 0) { ESP_LOGE(TAG, "Failed to stat file %s", full_path.c_str()); return ESP_ERR_NOT_FOUND; } out_size = st.st_size; return ESP_OK; } esp_err_t LittleFSHandler::create_directory(const std::string& relative_path) { if (!this->is_initialized_()) { ESP_LOGE(TAG, "LittleFS is not initialized"); return ESP_ERR_INVALID_STATE; } SemaphoreGuard guard(this->fs_mutex_); if (guard.take() != pdTRUE) { ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); return ESP_ERR_TIMEOUT; } std::string full_path = this->get_full_path(relative_path); if (mkdir(full_path.c_str(), 0755) != 0) { if (errno == EEXIST) { ESP_LOGW(TAG, "Directory %s already exists", full_path.c_str()); return ESP_OK; } ESP_LOGE(TAG, "Failed to create directory %s: %s", full_path.c_str(), strerror(errno)); return ESP_ERR_INVALID_STATE; } return ESP_OK; } esp_err_t LittleFSHandler::list_directory(const std::string& relative_path, std::vector& out_entries) { if (!this->is_initialized_()) { ESP_LOGE(TAG, "LittleFS is not initialized"); return ESP_ERR_INVALID_STATE; } SemaphoreGuard guard(this->fs_mutex_); if (guard.take() != pdTRUE) { ESP_LOGE(TAG, "Failed to acquire filesystem mutex"); return ESP_ERR_TIMEOUT; } std::string full_path = this->get_full_path(relative_path); DIR* dir = opendir(full_path.c_str()); if (dir == nullptr) { ESP_LOGE(TAG, "Failed to open directory %s", full_path.c_str()); return ESP_ERR_NOT_FOUND; } out_entries.clear(); struct dirent* entry; while ((entry = readdir(dir)) != nullptr) { // Skip . and .. if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } out_entries.push_back(entry->d_name); } closedir(dir); return ESP_OK; } // // Private methods // esp_err_t LittleFSHandler::open_file_(const std::string& relative_path, const char* flags, FILE*& out_file) { if (!this->is_initialized_()) { ESP_LOGE(TAG, "LittleFS is not initialized, cannot open file"); return ESP_ERR_INVALID_STATE; } std::string full_path = this->get_full_path(relative_path); FILE* file = fopen(full_path.c_str(), flags); if (file == nullptr) { // Use debug level if file doesn't exist (ENOENT), warning level for other errors if (errno == ENOENT) { ESP_LOGD(TAG, "File %s does not exist (flags %s)", full_path.c_str(), flags); } else { ESP_LOGW(TAG, "Failed to open file %s with flags %s: %s", full_path.c_str(), flags, strerror(errno)); } return ESP_ERR_NOT_FOUND; } out_file = file; return ESP_OK; } esp_err_t LittleFSHandler::close_file_(FILE* file) { if (file == nullptr) { return ESP_OK; } if (fclose(file) != 0) { ESP_LOGE(TAG, "Failed to close file: %s", strerror(errno)); return ESP_ERR_INVALID_STATE; } return ESP_OK; } esp_err_t LittleFSHandler::write_if_different_(FILE* file, const uint8_t* data, size_t size) { size_t out_bytes_written = 0; return this->write_if_different_(file, data, size, out_bytes_written); } esp_err_t LittleFSHandler::write_if_different_(FILE* file, const uint8_t* data, size_t size, size_t& out_bytes_written) { if (file == nullptr || data == nullptr) { ESP_LOGE(TAG, "Invalid parameters"); return ESP_ERR_INVALID_ARG; } // Get existing file size if (fseek(file, 0, SEEK_END) != 0) { ESP_LOGE(TAG, "Failed to seek to end of file"); return ESP_ERR_INVALID_STATE; } long file_size_long = ftell(file); if (file_size_long < 0) { ESP_LOGE(TAG, "Failed to get file size"); return ESP_ERR_INVALID_STATE; } size_t file_size = (size_t)file_size_long; if (fseek(file, 0, SEEK_SET) != 0) { ESP_LOGE(TAG, "Failed to seek to beginning of file"); return ESP_ERR_INVALID_STATE; } out_bytes_written = 0; size_t compare_size = (file_size < size) ? file_size : size; // Read entire file content for comparison std::vector existing_data; if (file_size > 0) { existing_data.resize(file_size); size_t bytes_read = fread(existing_data.data(), 1, file_size, file); if (bytes_read != file_size) { ESP_LOGE(TAG, "Failed to read existing file data"); return ESP_ERR_INVALID_STATE; } } // Compare and identify blocks that need updating std::vector block_needs_update((size + BLOCK_SIZE - 1) / BLOCK_SIZE, false); bool any_changes = false; for (size_t offset = 0; offset < compare_size; offset += BLOCK_SIZE) { size_t chunk_size = BLOCK_SIZE; if (offset + chunk_size > compare_size) { chunk_size = compare_size - offset; } if (memcmp(existing_data.data() + offset, data + offset, chunk_size) != 0) { block_needs_update[offset / BLOCK_SIZE] = true; any_changes = true; } } // Check if size changed or there are additional blocks to write if (size != file_size) { any_changes = true; } if (!any_changes) { ESP_LOGD(TAG, "File content unchanged, skipping write"); return ESP_OK; } // Seek to beginning to start writing if (fseek(file, 0, SEEK_SET) != 0) { ESP_LOGE(TAG, "Failed to seek to beginning for writing"); return ESP_ERR_INVALID_STATE; } // Write only changed blocks for (size_t offset = 0; offset < compare_size; offset += BLOCK_SIZE) { size_t block_index = offset / BLOCK_SIZE; if (!block_needs_update[block_index]) { // Skip unchanged block if (fseek(file, offset + BLOCK_SIZE, SEEK_SET) != 0) { // If at end of compare region, this is OK if (offset + BLOCK_SIZE > compare_size) { if (fseek(file, compare_size, SEEK_SET) != 0) { ESP_LOGE(TAG, "Failed to seek past unchanged block"); return ESP_ERR_INVALID_STATE; } } else { ESP_LOGE(TAG, "Failed to seek past unchanged block at %zu", offset); return ESP_ERR_INVALID_STATE; } } continue; } size_t chunk_size = BLOCK_SIZE; if (offset + chunk_size > compare_size) { chunk_size = compare_size - offset; } if (fseek(file, offset, SEEK_SET) != 0) { ESP_LOGE(TAG, "Failed to seek to offset %zu", offset); return ESP_ERR_INVALID_STATE; } size_t written = fwrite(data + offset, 1, chunk_size, file); if (written != chunk_size) { ESP_LOGE(TAG, "Failed to write block at offset %zu", offset); return ESP_ERR_INVALID_STATE; } out_bytes_written += written; } // Handle size differences if (size > file_size) { // Write additional data beyond original file size if (fseek(file, file_size, SEEK_SET) != 0) { ESP_LOGE(TAG, "Failed to seek to end for appending"); return ESP_ERR_INVALID_STATE; } size_t written = fwrite(data + file_size, 1, size - file_size, file); if (written != (size - file_size)) { ESP_LOGE(TAG, "Failed to write additional data"); return ESP_ERR_INVALID_STATE; } out_bytes_written += written; } else if (size < file_size) { // Truncate file to new size if (ftruncate(fileno(file), size) != 0) { ESP_LOGE(TAG, "Failed to truncate file to size %zu", size); return ESP_ERR_INVALID_STATE; } } // Flush to ensure data is written to storage if (fflush(file) != 0) { ESP_LOGE(TAG, "Failed to flush file after write"); return ESP_ERR_INVALID_STATE; } return ESP_OK; } bool LittleFSHandler::is_initialized_() const { return this->initialized_; }