#include #include #include #include #include #include #include #include #include #include #include static const char* TAG = "FileSystemWS"; namespace FileSystemWS { FileSystemHandler fsHandler; FileSystemHandler::FileSystemHandler() : transferIdCounter_(0), writeQueue_(nullptr), writerTaskHandle_(nullptr), uploadsMutex_(nullptr), writerTaskRunning_(false) { uploadsMutex_ = xSemaphoreCreateMutex(); } FileSystemHandler::~FileSystemHandler() { stopWriterTask(); if (uploadsMutex_) { vSemaphoreDelete(uploadsMutex_); } } void FileSystemHandler::startWriterTask() { if (writerTaskHandle_ != nullptr) { return; } writeQueue_ = xQueueCreate(FS_WRITE_QUEUE_SIZE, sizeof(WriteRequest)); if (!writeQueue_) { ESP_LOGE(TAG, "Failed to create write queue"); return; } writerTaskRunning_ = true; BaseType_t result = xTaskCreate(writerTaskFunc, "fs_writer", FS_WRITER_TASK_STACK_SIZE, this, FS_WRITER_TASK_PRIORITY, &writerTaskHandle_); if (result != pdPASS) { ESP_LOGE(TAG, "Failed to create writer task"); vQueueDelete(writeQueue_); writeQueue_ = nullptr; writerTaskRunning_ = false; } else { ESP_LOGI(TAG, "Writer task started"); } } void FileSystemHandler::stopWriterTask() { if (!writerTaskRunning_) { return; } writerTaskRunning_ = false; // Send a poison pill to wake up the task WriteRequest poison = {0, nullptr, 0, 0, false}; if (writeQueue_) { xQueueSend(writeQueue_, &poison, portMAX_DELAY); } // Wait for task to finish if (writerTaskHandle_) { vTaskDelay(pdMS_TO_TICKS(100)); writerTaskHandle_ = nullptr; } if (writeQueue_) { // Drain any remaining requests and free their data WriteRequest req; while (xQueueReceive(writeQueue_, &req, 0) == pdTRUE) { if (req.data) { free(req.data); } } vQueueDelete(writeQueue_); writeQueue_ = nullptr; } ESP_LOGI(TAG, "Writer task stopped"); } void FileSystemHandler::writerTaskFunc(void* param) { FileSystemHandler* self = static_cast(param); WriteRequest req; while (self->writerTaskRunning_) { if (xQueueReceive(self->writeQueue_, &req, pdMS_TO_TICKS(10)) == pdTRUE) { if (req.data == nullptr) { // Poison pill - exit break; } self->processWriteRequest(req); free(req.data); } } vTaskDelete(nullptr); } void FileSystemHandler::processWriteRequest(const WriteRequest& req) { xSemaphoreTake(uploadsMutex_, portMAX_DELAY); auto it = uploads_.find(req.transferId); if (it == uploads_.end()) { xSemaphoreGive(uploadsMutex_); return; } UploadState& state = it->second; if (state.hasError) { xSemaphoreGive(uploadsMutex_); return; } size_t bytesWritten = fwrite(req.data, 1, req.size, state.file); if (bytesWritten != req.size) { state.hasError = true; state.errorMessage = "Failed to write chunk"; xSemaphoreGive(uploadsMutex_); finalizeUpload(req.transferId, false, state.errorMessage); return; } state.chunksWritten++; ESP_LOGD(TAG, "Async write chunk %u/%u: %u bytes", state.chunksWritten, state.totalChunks, bytesWritten); // Periodic flush if (state.chunksWritten > 0 && state.chunksWritten % 64 == 0) { fflush(state.file); } bool shouldFinalize = req.isLastChunk; xSemaphoreGive(uploadsMutex_); if (shouldFinalize) { finalizeUpload(req.transferId, true); } } void FileSystemHandler::setSendCallbacks(SendMetadataCallback sendMetadata, SendCallback sendData, SendCompleteCallback sendComplete, SendUploadCompleteCallback sendUploadComplete) { sendMetadataCallback_ = sendMetadata; sendDataCallback_ = sendData; sendCompleteCallback_ = sendComplete; sendUploadCompleteCallback_ = sendUploadComplete; } void FileSystemHandler::cleanupExpiredTransfers() { uint32_t now = esp_timer_get_time() / 1000; auto dlIt = downloads_.begin(); while (dlIt != downloads_.end()) { if (now - dlIt->second.lastActivityTime > FS_TRANSFER_TIMEOUT_MS) { if (dlIt->second.file) { fclose(dlIt->second.file); } ESP_LOGW(TAG, "Download %u timed out", dlIt->first); if (sendCompleteCallback_) { socket_message_FSDownloadComplete complete = socket_message_FSDownloadComplete_init_zero; complete.transfer_id = dlIt->first; complete.success = false; strncpy(complete.error, "Transfer timed out", sizeof(complete.error) - 1); complete.total_chunks = dlIt->second.chunksSent; complete.file_size = dlIt->second.fileSize; sendCompleteCallback_(complete, dlIt->second.clientId); } dlIt = downloads_.erase(dlIt); } else { ++dlIt; } } xSemaphoreTake(uploadsMutex_, portMAX_DELAY); auto ulIt = uploads_.begin(); while (ulIt != uploads_.end()) { if (now - ulIt->second.lastActivityTime > FS_TRANSFER_TIMEOUT_MS) { if (ulIt->second.file) { fclose(ulIt->second.file); ulIt->second.file = nullptr; } std::string path = ulIt->second.path; uint32_t chunksReceived = ulIt->second.chunksReceived; int clientId = ulIt->second.clientId; uint32_t transferId = ulIt->first; ulIt = uploads_.erase(ulIt); xSemaphoreGive(uploadsMutex_); remove(path.c_str()); ESP_LOGW(TAG, "Upload %u timed out, deleted partial file", transferId); if (sendUploadCompleteCallback_) { socket_message_FSUploadComplete complete = socket_message_FSUploadComplete_init_zero; complete.transfer_id = transferId; complete.success = false; strncpy(complete.error, "Transfer timed out", sizeof(complete.error) - 1); complete.chunks_received = chunksReceived; sendUploadCompleteCallback_(complete, clientId); } xSemaphoreTake(uploadsMutex_, portMAX_DELAY); } else { ++ulIt; } } xSemaphoreGive(uploadsMutex_); } socket_message_FSDeleteResponse FileSystemHandler::handleDelete(const socket_message_FSDeleteRequest& req) { socket_message_FSDeleteResponse response = socket_message_FSDeleteResponse_init_zero; std::string path = req.path; ESP_LOGI(TAG, "Delete request: %s", path.c_str()); struct stat st; if (stat(path.c_str(), &st) != 0) { response.success = false; strncpy(response.error, "File or directory not found", sizeof(response.error) - 1); return response; } if (deleteRecursive(path)) { response.success = true; } else { response.success = false; strncpy(response.error, "Failed to delete", sizeof(response.error) - 1); } return response; } bool FileSystemHandler::deleteRecursive(const std::string& path) { struct stat st; if (stat(path.c_str(), &st) != 0) return false; if (S_ISDIR(st.st_mode)) { DIR* dir = opendir(path.c_str()); if (!dir) return false; struct dirent* entry; while ((entry = readdir(dir)) != nullptr) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } std::string childPath = path + "/" + entry->d_name; if (!deleteRecursive(childPath)) { closedir(dir); return false; } } closedir(dir); return rmdir(path.c_str()) == 0; } else { return remove(path.c_str()) == 0; } } socket_message_FSMkdirResponse FileSystemHandler::handleMkdir(const socket_message_FSMkdirRequest& req) { socket_message_FSMkdirResponse response = socket_message_FSMkdirResponse_init_zero; std::string path = req.path; ESP_LOGI(TAG, "Mkdir request: %s", path.c_str()); struct stat st; if (stat(path.c_str(), &st) == 0) { response.success = false; strncpy(response.error, "Path already exists", sizeof(response.error) - 1); return response; } if (FileSystem::mkdirRecursive(path.c_str())) { response.success = true; } else { response.success = false; strncpy(response.error, "Failed to create directory", sizeof(response.error) - 1); } return response; } void FileSystemHandler::listDirectory(const std::string& path, socket_message_FSListResponse& response) { // Root "/" is virtual - list mount points instead if (strcmp(path.c_str(), "/") == 0) { strncpy(response.directories[0].name, LITTLEFS_MOUNT_POINT + 1, sizeof(response.directories[0].name) - 1); strncpy(response.directories[1].name, SD_MOUNT_POINT + 1, sizeof(response.directories[1].name) - 1); response.directories_count = 2; return; } DIR* dir = opendir(path.c_str()); if (!dir) { return; } struct dirent* entry; int fileCount = 0; int dirCount = 0; while ((entry = readdir(dir)) != nullptr && fileCount < 20 && dirCount < 20) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } std::string fullPath = path + "/" + entry->d_name; struct stat st; if (stat(fullPath.c_str(), &st) != 0) continue; if (S_ISDIR(st.st_mode)) { if (dirCount < 20) { strncpy(response.directories[dirCount].name, entry->d_name, sizeof(response.directories[dirCount].name) - 1); dirCount++; } } else { if (fileCount < 20) { strncpy(response.files[fileCount].name, entry->d_name, sizeof(response.files[fileCount].name) - 1); response.files[fileCount].size = st.st_size; fileCount++; } } } closedir(dir); response.files_count = fileCount; response.directories_count = dirCount; } socket_message_FSListResponse FileSystemHandler::handleList(const socket_message_FSListRequest& req) { socket_message_FSListResponse response = socket_message_FSListResponse_init_zero; std::string path = req.path; ESP_LOGI(TAG, "List request: %s", path.c_str()); struct stat st; // Make sure that path exists, or that it is a root listing if (strcmp(path.c_str(), "/") != 0 && stat(path.c_str(), &st) != 0) { response.success = false; strncpy(response.error, "Path not found", sizeof(response.error) - 1); return response; } listDirectory(path, response); response.success = true; return response; } void FileSystemHandler::handleDownloadRequest(const socket_message_FSDownloadRequest& req, int clientId) { std::string path = req.path; ESP_LOGI(TAG, "Download request: %s", path.c_str()); struct stat st; if (stat(path.c_str(), &st) != 0 || S_ISDIR(st.st_mode)) { if (sendMetadataCallback_) { socket_message_FSDownloadMetadata metadata = socket_message_FSDownloadMetadata_init_zero; metadata.success = false; strncpy(metadata.error, "File not found", sizeof(metadata.error) - 1); sendMetadataCallback_(metadata, clientId); } return; } FILE* file = fopen(path.c_str(), "rb"); if (!file) { if (sendMetadataCallback_) { socket_message_FSDownloadMetadata metadata = socket_message_FSDownloadMetadata_init_zero; metadata.success = false; strncpy(metadata.error, "Cannot open file for reading", sizeof(metadata.error) - 1); sendMetadataCallback_(metadata, clientId); } return; } uint32_t fileSize = st.st_size; uint32_t chunkSize = FS_MAX_CHUNK_SIZE; uint32_t totalChunks = (fileSize + chunkSize - 1) / chunkSize; if (totalChunks == 0) totalChunks = 1; uint32_t transferId = generateTransferId(); if (sendMetadataCallback_) { socket_message_FSDownloadMetadata metadata = socket_message_FSDownloadMetadata_init_zero; metadata.transfer_id = transferId; metadata.success = true; metadata.file_size = fileSize; metadata.total_chunks = totalChunks; sendMetadataCallback_(metadata, clientId); } DownloadState state; state.path = path; state.file = file; state.fileSize = fileSize; state.chunkSize = chunkSize; state.totalChunks = totalChunks; state.chunksSent = 0; state.lastActivityTime = esp_timer_get_time() / 1000; state.clientId = clientId; downloads_[transferId] = state; ESP_LOGI(TAG, "Download started: %s, size=%u, chunks=%u, id=%u", path.c_str(), fileSize, totalChunks, transferId); while (sendNextDownloadChunk(transferId)) { taskYIELD(); } } bool FileSystemHandler::sendNextDownloadChunk(uint32_t transferId) { auto it = downloads_.find(transferId); if (it == downloads_.end()) { return false; } DownloadState& state = it->second; state.lastActivityTime = esp_timer_get_time() / 1000; if (state.chunksSent >= state.totalChunks) { if (sendCompleteCallback_) { socket_message_FSDownloadComplete complete = socket_message_FSDownloadComplete_init_zero; complete.transfer_id = transferId; complete.success = true; complete.total_chunks = state.totalChunks; complete.file_size = state.fileSize; sendCompleteCallback_(complete, state.clientId); } fclose(state.file); downloads_.erase(it); ESP_LOGI(TAG, "Download completed: %u", transferId); return false; } auto data = new socket_message_FSDownloadData(); memset(data, 0, sizeof(socket_message_FSDownloadData)); data->transfer_id = transferId; data->chunk_index = state.chunksSent; uint32_t bytesToRead = state.chunkSize; uint32_t position = state.chunksSent * state.chunkSize; if (position + bytesToRead > state.fileSize) { bytesToRead = state.fileSize - position; } // Allocate buffer for FT_POINTER data field data->data = (pb_bytes_array_t*)malloc(PB_BYTES_ARRAY_T_ALLOCSIZE(bytesToRead)); if (!data->data) { delete data; if (sendCompleteCallback_) { socket_message_FSDownloadComplete complete = socket_message_FSDownloadComplete_init_zero; complete.transfer_id = transferId; complete.success = false; strncpy(complete.error, "Memory allocation failed", sizeof(complete.error) - 1); complete.total_chunks = state.chunksSent; complete.file_size = state.fileSize; sendCompleteCallback_(complete, state.clientId); } fclose(state.file); downloads_.erase(it); ESP_LOGE(TAG, "Download failed - memory allocation: %u", transferId); return false; } size_t bytesRead = fread(data->data->bytes, 1, bytesToRead, state.file); if (bytesRead == 0 && bytesToRead > 0) { free(data->data); delete data; if (sendCompleteCallback_) { socket_message_FSDownloadComplete complete = socket_message_FSDownloadComplete_init_zero; complete.transfer_id = transferId; complete.success = false; strncpy(complete.error, "Failed to read file", sizeof(complete.error) - 1); complete.total_chunks = state.chunksSent; complete.file_size = state.fileSize; sendCompleteCallback_(complete, state.clientId); } fclose(state.file); downloads_.erase(it); ESP_LOGE(TAG, "Download failed - read error: %u", transferId); return false; } data->data->size = bytesRead; if (sendDataCallback_) { sendDataCallback_(*data, state.clientId); } free(data->data); delete data; state.chunksSent++; ESP_LOGD(TAG, "Download chunk %u/%u sent: %u bytes", state.chunksSent, state.totalChunks, bytesRead); return true; } socket_message_FSUploadStartResponse FileSystemHandler::handleUploadStart(const socket_message_FSUploadStart& req, int clientId) { socket_message_FSUploadStartResponse response = socket_message_FSUploadStartResponse_init_zero; std::string path = req.path; ESP_LOGI(TAG, "Upload start request: %s, size=%u, chunks=%u", path.c_str(), req.file_size, req.total_chunks); // Check available space on the target filesystem if (path.find(SD_MOUNT_POINT) != 0) { // LittleFS path size_t fs_total = 0, fs_used = 0; esp_littlefs_info("spiffs", &fs_total, &fs_used); size_t freeSpace = fs_total - fs_used; if (freeSpace < req.file_size + 4096) { response.success = false; strncpy(response.error, "Insufficient storage space", sizeof(response.error) - 1); return response; } } // TODO: SD card space check skipped - FAT doesn't have a simple API for this size_t lastSlash = path.find_last_of('/'); if (lastSlash != std::string::npos && lastSlash > 0) { std::string parentDir = path.substr(0, lastSlash); struct stat st; if (stat(parentDir.c_str(), &st) != 0) { response.success = false; strncpy(response.error, "Parent directory does not exist", sizeof(response.error) - 1); return response; } } FILE* file = fopen(path.c_str(), "wb"); if (!file) { ESP_LOGE(TAG, "fopen failed for '%s': %s (errno=%d)", path.c_str(), strerror(errno), errno); response.success = false; snprintf(response.error, sizeof(response.error) - 1, "Cannot open file: %s", strerror(errno)); return response; } // Set larger buffer for better write performance setvbuf(file, nullptr, _IOFBF, 32 * 1024); uint32_t transferId = generateTransferId(); UploadState state; state.path = path; state.file = file; state.fileSize = req.file_size; state.totalChunks = req.total_chunks; state.chunksReceived = 0; state.chunksWritten = 0; state.bytesReceived = 0; state.lastActivityTime = esp_timer_get_time() / 1000; state.clientId = clientId; state.hasError = false; xSemaphoreTake(uploadsMutex_, portMAX_DELAY); uploads_[transferId] = state; xSemaphoreGive(uploadsMutex_); response.success = true; response.transfer_id = transferId; ESP_LOGI(TAG, "Upload started: %s, id=%u", path.c_str(), transferId); return response; } void FileSystemHandler::handleUploadData(const socket_message_FSUploadData& req) { uint32_t transferId = req.transfer_id; // Auto-start writer task if not running if (!writerTaskRunning_) { startWriterTask(); } xSemaphoreTake(uploadsMutex_, portMAX_DELAY); auto it = uploads_.find(transferId); if (it == uploads_.end()) { xSemaphoreGive(uploadsMutex_); ESP_LOGW(TAG, "Upload data for unknown transfer: %u", transferId); return; } UploadState& state = it->second; state.lastActivityTime = esp_timer_get_time() / 1000; if (state.hasError) { xSemaphoreGive(uploadsMutex_); return; } if (req.chunk_index != state.chunksReceived) { ESP_LOGW(TAG, "Upload chunk out of order: expected %u, got %u", state.chunksReceived, req.chunk_index); } if (!req.data || req.data->size == 0) { state.hasError = true; state.errorMessage = "Empty or invalid data chunk"; xSemaphoreGive(uploadsMutex_); finalizeUpload(transferId, false, state.errorMessage); return; } // Copy data for async write WriteRequest writeReq; writeReq.transferId = transferId; writeReq.size = req.data->size; writeReq.chunkIndex = req.chunk_index; writeReq.data = static_cast(malloc(req.data->size)); if (!writeReq.data) { state.hasError = true; state.errorMessage = "Memory allocation failed"; xSemaphoreGive(uploadsMutex_); finalizeUpload(transferId, false, state.errorMessage); return; } memcpy(writeReq.data, req.data->bytes, req.data->size); state.chunksReceived++; state.bytesReceived += req.data->size; writeReq.isLastChunk = (state.chunksReceived >= state.totalChunks); ESP_LOGD(TAG, "Queuing chunk %u/%u: %u bytes", state.chunksReceived, state.totalChunks, req.data->size); xSemaphoreGive(uploadsMutex_); // Check queue is valid if (!writeQueue_) { ESP_LOGE(TAG, "Write queue not initialized"); free(writeReq.data); finalizeUpload(transferId, false, "Write queue not initialized"); return; } // Try to queue (non-blocking) - if full, do sync write to avoid blocking HTTP server if (xQueueSend(writeQueue_, &writeReq, 0) != pdTRUE) { ESP_LOGD(TAG, "Queue full, doing sync write for chunk %u", writeReq.chunkIndex); processWriteRequest(writeReq); free(writeReq.data); } } void FileSystemHandler::finalizeUpload(uint32_t transferId, bool success, const std::string& error) { xSemaphoreTake(uploadsMutex_, portMAX_DELAY); auto it = uploads_.find(transferId); if (it == uploads_.end()) { xSemaphoreGive(uploadsMutex_); return; } UploadState& state = it->second; if (state.file) { fclose(state.file); state.file = nullptr; } std::string path = state.path; uint32_t bytesReceived = state.bytesReceived; uint32_t chunksReceived = state.chunksReceived; int clientId = state.clientId; uploads_.erase(it); xSemaphoreGive(uploadsMutex_); if (!success) { remove(path.c_str()); ESP_LOGW(TAG, "Upload failed, deleted partial file: %s", path.c_str()); } else { ESP_LOGI(TAG, "Upload completed: %s (%u bytes)", path.c_str(), bytesReceived); } if (sendUploadCompleteCallback_) { socket_message_FSUploadComplete complete = socket_message_FSUploadComplete_init_zero; complete.transfer_id = transferId; complete.success = success; if (!error.empty()) { strncpy(complete.error, error.c_str(), sizeof(complete.error) - 1); } complete.chunks_received = chunksReceived; sendUploadCompleteCallback_(complete, clientId); } } socket_message_FSCancelTransferResponse FileSystemHandler::handleCancelTransfer( const socket_message_FSCancelTransfer& req) { socket_message_FSCancelTransferResponse response = socket_message_FSCancelTransferResponse_init_zero; uint32_t transferId = req.transfer_id; response.transfer_id = transferId; auto dlIt = downloads_.find(transferId); if (dlIt != downloads_.end()) { if (dlIt->second.file) { fclose(dlIt->second.file); } downloads_.erase(dlIt); response.success = true; ESP_LOGI(TAG, "Download cancelled: %u", transferId); return response; } xSemaphoreTake(uploadsMutex_, portMAX_DELAY); auto ulIt = uploads_.find(transferId); if (ulIt != uploads_.end()) { if (ulIt->second.file) { fclose(ulIt->second.file); ulIt->second.file = nullptr; } std::string path = ulIt->second.path; uploads_.erase(ulIt); xSemaphoreGive(uploadsMutex_); remove(path.c_str()); response.success = true; ESP_LOGI(TAG, "Upload cancelled: %u", transferId); return response; } xSemaphoreGive(uploadsMutex_); response.success = false; return response; } void FileSystemHandler::processPendingDownloads() {} } // namespace FileSystemWS