/*! 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 = "FileServer"; /* Handler to redirect incoming GET request for /index.html to / * This can be overridden by uploading file with same name */ static esp_err_t index_html_get_handler(httpd_req_t *req) { httpd_resp_set_status(req, "307 Temporary Redirect"); httpd_resp_set_hdr(req, "Location", "/"); httpd_resp_send(req, NULL, 0); // Response body can be empty return ESP_OK; } #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) { if (IS_FILE_EXT(filename, ".pdf")) { return httpd_resp_set_type(req, "application/pdf"); } else if (IS_FILE_EXT(filename, ".html")) { return httpd_resp_set_type(req, "text/html"); } else if (IS_FILE_EXT(filename, ".jpeg")) { return httpd_resp_set_type(req, "image/jpeg"); } else if (IS_FILE_EXT(filename, ".png")) { return httpd_resp_set_type(req, "image/png"); } else if (IS_FILE_EXT(filename, ".ico")) { return httpd_resp_set_type(req, "image/x-icon"); } else if (IS_FILE_EXT(filename, ".css")) { return httpd_resp_set_type(req, "text/css"); } else if (IS_FILE_EXT(filename, ".woff2")) { return httpd_resp_set_type(req, "font/woff2"); } /* This is a limited set only */ /* For any other type always set as plain text */ return httpd_resp_set_type(req, "text/plain"); } /* Copies the full path into destination buffer and returns * pointer to path (skipping the preceding base path) */ static const char* get_path_from_uri(char *dest, const char *base_path, const char *uri, size_t destsize) { const size_t base_pathlen = strlen(base_path); size_t pathlen = strlen(uri); const char *quest = strchr(uri, '?'); if (quest) { pathlen = MIN(pathlen, quest - uri); } const char *hash = strchr(uri, '#'); if (hash) { pathlen = MIN(pathlen, hash - uri); } if (base_pathlen + pathlen + 1 > destsize) { /* Full path string won't fit into destination buffer */ return NULL; } /* Construct full path (base + path) */ strcpy(dest, base_path); strlcpy(dest + base_pathlen, uri + (sizeof("/files")-1), pathlen + 1 - (sizeof("/files")-1)); /* Return pointer to path, skipping the base */ return dest + base_pathlen; } /* 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, "Files 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 download a file kept on the server */ esp_err_t download_get_handler(httpd_req_t *req) { char filepath[FILE_PATH_MAX]; FILE *fd = NULL; struct stat file_stat; const char *filename = get_path_from_uri(filepath, ((struct file_server_data*) req->user_ctx)->base_path2, req->uri, sizeof(filepath)); ESP_LOGW(TAG, "FILE_GET_URI %s", req->uri); ESP_LOGW(TAG, "FILE_GET_FILEPATH %s", filepath); ESP_LOGW(TAG, "FILE_GET_FILENAME %s", filename); if (!filename) { ESP_LOGE(TAG, "Filename is too long"); /* Respond with 500 Internal Server Error */ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long"); return ESP_FAIL; } /* If name has trailing '/', respond with directory contents */ if (filename[strlen(filename) - 1] == '/') { return http_resp_dir_html(req, filepath); } if (stat(filepath, &file_stat) == -1) { /* If file not present on SPIFFS check if URI * corresponds to one of the hardcoded paths */ if (strcmp(filename, "/") == 0) { return index_html_get_handler(req); } ESP_LOGE(TAG, "Failed to stat file : %s", filepath); /* Respond with 404 Not Found */ httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist"); return ESP_FAIL; } fd = fopen(filepath, "r"); if (!fd) { ESP_LOGE(TAG, "Failed to read existing file : %s", filepath); /* Respond with 500 Internal Server Error */ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file"); return ESP_FAIL; } ESP_LOGI(TAG, "Sending file : %s (%ld bytes)...", filename, file_stat.st_size); set_content_type_from_file(req, filename); /* Retrieve the pointer to scratch buffer for temporary storage */ char *chunk = ((struct file_server_data*) req->user_ctx)->scratch; size_t chunksize; do { /* Read file in chunks into the scratch buffer */ chunksize = fread(chunk, 1, SCRATCH_BUFSIZE, fd); if (chunksize > 0) { /* Send the buffer contents as HTTP response chunk */ if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) { fclose(fd); ESP_LOGE(TAG, "File sending failed!"); /* Abort sending file */ httpd_resp_sendstr_chunk(req, NULL); /* Respond with 500 Internal Server Error */ httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file"); return ESP_FAIL; } } /* Keep looping till the whole file is sent */ } while (chunksize != 0); /* Close file after sending complete */ fclose(fd); ESP_LOGI(TAG, "File sending complete"); /* Respond with an empty chunk to signal HTTP response completion */ #ifdef CONFIG_EXAMPLE_HTTPD_CONN_CLOSE_HEADER httpd_resp_set_hdr(req, "Connection", "close"); #endif httpd_resp_send_chunk(req, NULL, 0); return ESP_OK; } /* Handler to upload a file onto the server */ 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_path2, req->uri + sizeof("/upload") - 1, sizeof(filepath)); ESP_LOGW(TAG, "FILE_POST_URI %s", req->uri); ESP_LOGW(TAG, "FILE_POST_FILEPATH %s", filepath); ESP_LOGW(TAG, "FILE_POST_FILENAME %s", filename); 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", "/files/"); #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 */ 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_path2, 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", "/files/"); #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; }