feat: implement LittleFSHandler and FSGuard for improved file management

This commit is contained in:
GW_MC
2026-01-30 15:23:44 +08:00
parent b6c4477c46
commit 2a5088bec3
7 changed files with 821 additions and 85 deletions

552
main/io/fs_handler.cpp Normal file
View File

@@ -0,0 +1,552 @@
#include "io/fs_handler.h"
#include <sys/stat.h>
#include <dirent.h>
#include <errno.h>
#include <cstring>
#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<std::string>& 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<uint8_t> 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<bool> 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_;
}