From 5ab7c053b9862f28bb7a450449585b9b42786b24 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 14 Nov 2022 08:25:04 +0200 Subject: [PATCH] spiffs added, http server prepare to multi fs operations --- CMakeLists.txt | 2 + include/HTTPServer.h | 9 ++ include/spifs.h | 33 +++++ src/FileServer.c | 277 ++++++++++++++++++++++++++++++++++++++ src/HTTPServer.c | 11 +- src/SystemConfiguration.c | 4 +- src/spifs.c | 67 +++++++++ 7 files changed, 394 insertions(+), 9 deletions(-) create mode 100644 include/spifs.h create mode 100644 src/FileServer.c create mode 100644 src/spifs.c diff --git a/CMakeLists.txt b/CMakeLists.txt index b833856..7aa6080 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,7 @@ idf_component_register( SRCS "src/SystemConfiguration.c" "src/romfs.c" + "src/spifs.c" "src/HTTPServer.c" "src/HTTPPrintSystem.c" "src/HTTPPostSystem.c" @@ -20,6 +21,7 @@ idf_component_register( REQUIRES nvs_flash libespfs + spiffs esp_http_server mbedtls lwip diff --git a/include/HTTPServer.h b/include/HTTPServer.h index b538c32..ad7131a 100644 --- a/include/HTTPServer.h +++ b/include/HTTPServer.h @@ -47,6 +47,15 @@ #define MAX_DYNVAR_LENGTH 256 #define MAX_INCFILE_LENGTH 1024 +/* Max length a file path can have on storage */ +#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + CONFIG_SPIFFS_OBJ_NAME_LEN) +#define MAX_FILE_SIZE (200*1024) // 200 KB +#define MAX_FILE_SIZE_STR "200KB" + +/* Scratch buffer size */ +#define SCRATCH_BUFSIZE 4096 +#define AUTH_DATA_MAX_LENGTH 16 + #define HTTP_SERVER_DEBUG_LEVEL 0 typedef enum diff --git a/include/spifs.h b/include/spifs.h new file mode 100644 index 0000000..20d2c49 --- /dev/null +++ b/include/spifs.h @@ -0,0 +1,33 @@ + /*! Copyright 2022 Bogdan Pilyugin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * \file spifs.h + * \version 1.0 + * \date 2022-11-13 + * \author Bogdan Pilyugin + * \brief + * \details + * \copyright Apache License, Version 2.0 + */ + +#ifndef COMPONENTS_WEBGUIAPP_INCLUDE_SPIFS_H_ +#define COMPONENTS_WEBGUIAPP_INCLUDE_SPIFS_H_ + +#include "esp_err.h" + +esp_err_t init_spi_fs(const char *root); + + + +#endif /* COMPONENTS_WEBGUIAPP_INCLUDE_SPIFS_H_ */ diff --git a/src/FileServer.c b/src/FileServer.c new file mode 100644 index 0000000..3213230 --- /dev/null +++ b/src/FileServer.c @@ -0,0 +1,277 @@ + /*! Copyright 2022 Bogdan Pilyugin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * \file FileServer.c + * \version 1.0 + * \date 2022-11-13 + * \author Bogdan Pilyugin + * \brief + * \details + * \copyright Apache License, Version 2.0 + */ + + +#include "HTTPServer.h" + +static const char *TAG = "HTTPServer"; + +/* Send HTTP response with a run-time generated html consisting of + * a list of all files and folders under the requested path. + * In case of SPIFFS this returns empty list when path is any + * string other than '/', since SPIFFS doesn't support directories */ +static esp_err_t http_resp_dir_html(httpd_req_t *req, const char *dirpath) +{ + char entrypath[FILE_PATH_MAX]; + char entrysize[16]; + const char *entrytype; + + struct dirent *entry; + struct stat entry_stat; + + DIR *dir = opendir(dirpath); + const size_t dirpath_len = strlen(dirpath); + + /* Retrieve the base path of file storage to construct the full path */ + strlcpy(entrypath, dirpath, sizeof(entrypath)); + + if (!dir) { + ESP_LOGE(TAG, "Failed to stat dir : %s", dirpath); + /* Respond with 404 Not Found */ + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Directory does not exist"); + return ESP_FAIL; + } + + /* Send HTML file header */ + httpd_resp_sendstr_chunk(req, ""); + + /* Get handle to embedded file upload script */ + extern const unsigned char upload_script_start[] asm("_binary_upload_script_html_start"); + extern const unsigned char upload_script_end[] asm("_binary_upload_script_html_end"); + const size_t upload_script_size = (upload_script_end - upload_script_start); + + /* Add file upload form and script which on execution sends a POST request to /upload */ + httpd_resp_send_chunk(req, (const char *)upload_script_start, upload_script_size); + + /* Send file-list table definition and column labels */ + httpd_resp_sendstr_chunk(req, + "" + "" + "" + ""); + + /* Iterate over all files / folders and fetch their names and sizes */ + while ((entry = readdir(dir)) != NULL) { + entrytype = (entry->d_type == DT_DIR ? "directory" : "file"); + + strlcpy(entrypath + dirpath_len, entry->d_name, sizeof(entrypath) - dirpath_len); + if (stat(entrypath, &entry_stat) == -1) { + ESP_LOGE(TAG, "Failed to stat %s : %s", entrytype, entry->d_name); + continue; + } + sprintf(entrysize, "%ld", entry_stat.st_size); + ESP_LOGI(TAG, "Found %s : %s (%s bytes)", entrytype, entry->d_name, entrysize); + + /* Send chunk of HTML file containing table entries with file name and size */ + httpd_resp_sendstr_chunk(req, "\n"); + } + closedir(dir); + + /* Finish the file list table */ + httpd_resp_sendstr_chunk(req, "
NameTypeSize (Bytes)Delete
uri); + httpd_resp_sendstr_chunk(req, entry->d_name); + if (entry->d_type == DT_DIR) { + httpd_resp_sendstr_chunk(req, "/"); + } + httpd_resp_sendstr_chunk(req, "\">"); + httpd_resp_sendstr_chunk(req, entry->d_name); + httpd_resp_sendstr_chunk(req, ""); + httpd_resp_sendstr_chunk(req, entrytype); + httpd_resp_sendstr_chunk(req, ""); + httpd_resp_sendstr_chunk(req, entrysize); + httpd_resp_sendstr_chunk(req, ""); + httpd_resp_sendstr_chunk(req, "
uri); + httpd_resp_sendstr_chunk(req, entry->d_name); + httpd_resp_sendstr_chunk(req, "\">
"); + httpd_resp_sendstr_chunk(req, "
"); + + /* Send remaining chunk of HTML file to complete it */ + httpd_resp_sendstr_chunk(req, ""); + + /* Send empty chunk to signal HTTP response completion */ + httpd_resp_sendstr_chunk(req, NULL); + return ESP_OK; +} + +/* Handler to upload a file onto the server */ +static esp_err_t upload_post_handler(httpd_req_t *req) +{ + char filepath[FILE_PATH_MAX]; + FILE *fd = NULL; + struct stat file_stat; + + /* Skip leading "/upload" from URI to get filename */ + /* Note sizeof() counts NULL termination hence the -1 */ + const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path, + req->uri + sizeof("/upload") - 1, sizeof(filepath)); + if (!filename) { + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long"); + return ESP_FAIL; + } + + /* Filename cannot have a trailing '/' */ + if (filename[strlen(filename) - 1] == '/') { + ESP_LOGE(TAG, "Invalid filename : %s", filename); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Invalid filename"); + return ESP_FAIL; + } + + if (stat(filepath, &file_stat) == 0) { + ESP_LOGE(TAG, "File already exists : %s", filepath); + /* Respond with 400 Bad Request */ + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "File already exists"); + return ESP_FAIL; + } + + /* File cannot be larger than a limit */ + if (req->content_len > MAX_FILE_SIZE) { + ESP_LOGE(TAG, "File too large : %d bytes", req->content_len); + /* Respond with 400 Bad Request */ + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, + "File size must be less than " + MAX_FILE_SIZE_STR "!"); + /* Return failure to close underlying connection else the + * incoming file content will keep the socket busy */ + return ESP_FAIL; + } + + fd = fopen(filepath, "w"); + if (!fd) { + ESP_LOGE(TAG, "Failed to create file : %s", filepath); + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to create file"); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Receiving file : %s...", filename); + + /* Retrieve the pointer to scratch buffer for temporary storage */ + char *buf = ((struct file_server_data *)req->user_ctx)->scratch; + int received; + + /* Content length of the request gives + * the size of the file being uploaded */ + int remaining = req->content_len; + + while (remaining > 0) { + + ESP_LOGI(TAG, "Remaining size : %d", remaining); + /* Receive the file part by part into a buffer */ + if ((received = httpd_req_recv(req, buf, MIN(remaining, SCRATCH_BUFSIZE))) <= 0) { + if (received == HTTPD_SOCK_ERR_TIMEOUT) { + /* Retry if timeout occurred */ + continue; + } + + /* In case of unrecoverable error, + * close and delete the unfinished file*/ + fclose(fd); + unlink(filepath); + + ESP_LOGE(TAG, "File reception failed!"); + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file"); + return ESP_FAIL; + } + + /* Write buffer content to file on storage */ + if (received && (received != fwrite(buf, 1, received, fd))) { + /* Couldn't write everything to file! + * Storage may be full? */ + fclose(fd); + unlink(filepath); + + ESP_LOGE(TAG, "File write failed!"); + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to write file to storage"); + return ESP_FAIL; + } + + /* Keep track of remaining size of + * the file left to be uploaded */ + remaining -= received; + } + + /* Close file upon upload completion */ + fclose(fd); + ESP_LOGI(TAG, "File reception complete"); + + /* Redirect onto root to see the updated file list */ + httpd_resp_set_status(req, "303 See Other"); + httpd_resp_set_hdr(req, "Location", "/"); +#ifdef CONFIG_EXAMPLE_HTTPD_CONN_CLOSE_HEADER + httpd_resp_set_hdr(req, "Connection", "close"); +#endif + httpd_resp_sendstr(req, "File uploaded successfully"); + return ESP_OK; +} + +/* Handler to delete a file from the server */ +static esp_err_t delete_post_handler(httpd_req_t *req) +{ + char filepath[FILE_PATH_MAX]; + struct stat file_stat; + + /* Skip leading "/delete" from URI to get filename */ + /* Note sizeof() counts NULL termination hence the -1 */ + const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path, + req->uri + sizeof("/delete") - 1, sizeof(filepath)); + if (!filename) { + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long"); + return ESP_FAIL; + } + + /* Filename cannot have a trailing '/' */ + if (filename[strlen(filename) - 1] == '/') { + ESP_LOGE(TAG, "Invalid filename : %s", filename); + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Invalid filename"); + return ESP_FAIL; + } + + if (stat(filepath, &file_stat) == -1) { + ESP_LOGE(TAG, "File does not exist : %s", filename); + /* Respond with 400 Bad Request */ + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "File does not exist"); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Deleting file : %s", filename); + /* Delete file */ + unlink(filepath); + + /* Redirect onto root to see the updated file list */ + httpd_resp_set_status(req, "303 See Other"); + httpd_resp_set_hdr(req, "Location", "/"); +#ifdef CONFIG_EXAMPLE_HTTPD_CONN_CLOSE_HEADER + httpd_resp_set_hdr(req, "Connection", "close"); +#endif + httpd_resp_sendstr(req, "File deleted successfully"); + return ESP_OK; +} + + + diff --git a/src/HTTPServer.c b/src/HTTPServer.c index d617898..582d8e0 100644 --- a/src/HTTPServer.c +++ b/src/HTTPServer.c @@ -29,15 +29,7 @@ const char GZIP_SIGN[] = { 0x1f, 0x8b, 0x08 }; static esp_err_t GETHandler(httpd_req_t *req); static esp_err_t CheckAuth(httpd_req_t *req); -/* Max length a file path can have on storage */ -#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + CONFIG_SPIFFS_OBJ_NAME_LEN) -/* Max size of an individual file. Make sure this - * value is same as that set in upload_script.html */ - -/* Scratch buffer size */ -#define SCRATCH_BUFSIZE 4096 -#define AUTH_DATA_MAX_LENGTH 16 struct file_server_data { @@ -104,6 +96,7 @@ static esp_err_t CheckAuth(httpd_req_t *req) #define IS_FILE_EXT(filename, ext) \ (strcasecmp(&filename[strlen(filename) - sizeof(ext) + 1], ext) == 0) + /* Set HTTP response content type according to file extension */ static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filename) @@ -460,6 +453,8 @@ file_send_error: return ESP_FAIL; } + + static httpd_handle_t start_webserver(void) { httpd_handle_t server = NULL; diff --git a/src/SystemConfiguration.c b/src/SystemConfiguration.c index 42c1fb9..18a2479 100644 --- a/src/SystemConfiguration.c +++ b/src/SystemConfiguration.c @@ -36,6 +36,7 @@ #include "SystemConfiguration.h" #include "romfs.h" +#include "spifs.h" #include "NetTransport.h" #include "Helpers.h" #include "HTTPServer.h" @@ -102,8 +103,9 @@ esp_err_t WebGuiAppInit(void) } ESP_ERROR_CHECK(InitSysConfig()); - //init rom file system + //init file systems init_rom_fs("/espfs"); + init_spi_fs("/data"); #if CONFIG_WEBGUIAPP_GPRS_ENABLE diff --git a/src/spifs.c b/src/spifs.c new file mode 100644 index 0000000..0e114ca --- /dev/null +++ b/src/spifs.c @@ -0,0 +1,67 @@ + /*! Copyright 2022 Bogdan Pilyugin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * \file spifs.c + * \version 1.0 + * \date 2022-11-13 + * \author Bogdan Pilyugin + * \brief + * \details + * \copyright Apache License, Version 2.0 + */ + +#include "spifs.h" +#include +#include +#include "esp_log.h" +#include "esp_err.h" +#include "sdkconfig.h" +#include "esp_spiffs.h" + + +static const char *TAG = "SPIFS"; + +esp_err_t init_spi_fs(const char *root) +{ + ESP_LOGI(TAG, "Initializing SPIFFS"); + + esp_vfs_spiffs_conf_t conf = { + .base_path = root, + .partition_label = NULL, + .max_files = 5, // This sets the maximum number of files that can be open at the same time + .format_if_mount_failed = true + }; + + esp_err_t ret = esp_vfs_spiffs_register(&conf); + if (ret != ESP_OK) { + if (ret == ESP_FAIL) { + ESP_LOGE(TAG, "Failed to mount or format filesystem"); + } else if (ret == ESP_ERR_NOT_FOUND) { + ESP_LOGE(TAG, "Failed to find SPIFFS partition"); + } else { + ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret)); + } + return ret; + } + + size_t total = 0, used = 0; + ret = esp_spiffs_info(NULL, &total, &used); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret)); + return ret; + } + + ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used); + return ESP_OK; +}