1 /* HTTP File Server Example
2 
3    This example code is in the Public Domain (or CC0 licensed, at your option.)
4 
5    Unless required by applicable law or agreed to in writing, this
6    software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
7    CONDITIONS OF ANY KIND, either express or implied.
8 */
9 
10 #include <stdio.h>
11 #include <string.h>
12 #include <sys/param.h>
13 #include <sys/unistd.h>
14 #include <sys/stat.h>
15 #include <dirent.h>
16 
17 #include "esp_err.h"
18 #include "esp_log.h"
19 
20 #include "esp_vfs.h"
21 #include "esp_spiffs.h"
22 #include "esp_http_server.h"
23 
24 /* Max length a file path can have on storage */
25 #define FILE_PATH_MAX (ESP_VFS_PATH_MAX + CONFIG_SPIFFS_OBJ_NAME_LEN)
26 
27 /* Max size of an individual file. Make sure this
28  * value is same as that set in upload_script.html */
29 #define MAX_FILE_SIZE   (200*1024) // 200 KB
30 #define MAX_FILE_SIZE_STR "200KB"
31 
32 /* Scratch buffer size */
33 #define SCRATCH_BUFSIZE  8192
34 
35 struct file_server_data {
36     /* Base path of file storage */
37     char base_path[ESP_VFS_PATH_MAX + 1];
38 
39     /* Scratch buffer for temporary storage during file transfer */
40     char scratch[SCRATCH_BUFSIZE];
41 };
42 
43 static const char *TAG = "file_server";
44 
45 /* Handler to redirect incoming GET request for /index.html to /
46  * This can be overridden by uploading file with same name */
index_html_get_handler(httpd_req_t * req)47 static esp_err_t index_html_get_handler(httpd_req_t *req)
48 {
49     httpd_resp_set_status(req, "307 Temporary Redirect");
50     httpd_resp_set_hdr(req, "Location", "/");
51     httpd_resp_send(req, NULL, 0);  // Response body can be empty
52     return ESP_OK;
53 }
54 
55 /* Handler to respond with an icon file embedded in flash.
56  * Browsers expect to GET website icon at URI /favicon.ico.
57  * This can be overridden by uploading file with same name */
favicon_get_handler(httpd_req_t * req)58 static esp_err_t favicon_get_handler(httpd_req_t *req)
59 {
60     extern const unsigned char favicon_ico_start[] asm("_binary_favicon_ico_start");
61     extern const unsigned char favicon_ico_end[]   asm("_binary_favicon_ico_end");
62     const size_t favicon_ico_size = (favicon_ico_end - favicon_ico_start);
63     httpd_resp_set_type(req, "image/x-icon");
64     httpd_resp_send(req, (const char *)favicon_ico_start, favicon_ico_size);
65     return ESP_OK;
66 }
67 
68 /* Send HTTP response with a run-time generated html consisting of
69  * a list of all files and folders under the requested path.
70  * In case of SPIFFS this returns empty list when path is any
71  * string other than '/', since SPIFFS doesn't support directories */
http_resp_dir_html(httpd_req_t * req,const char * dirpath)72 static esp_err_t http_resp_dir_html(httpd_req_t *req, const char *dirpath)
73 {
74     char entrypath[FILE_PATH_MAX];
75     char entrysize[16];
76     const char *entrytype;
77 
78     struct dirent *entry;
79     struct stat entry_stat;
80 
81     DIR *dir = opendir(dirpath);
82     const size_t dirpath_len = strlen(dirpath);
83 
84     /* Retrieve the base path of file storage to construct the full path */
85     strlcpy(entrypath, dirpath, sizeof(entrypath));
86 
87     if (!dir) {
88         ESP_LOGE(TAG, "Failed to stat dir : %s", dirpath);
89         /* Respond with 404 Not Found */
90         httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Directory does not exist");
91         return ESP_FAIL;
92     }
93 
94     /* Send HTML file header */
95     httpd_resp_sendstr_chunk(req, "<!DOCTYPE html><html><body>");
96 
97     /* Get handle to embedded file upload script */
98     extern const unsigned char upload_script_start[] asm("_binary_upload_script_html_start");
99     extern const unsigned char upload_script_end[]   asm("_binary_upload_script_html_end");
100     const size_t upload_script_size = (upload_script_end - upload_script_start);
101 
102     /* Add file upload form and script which on execution sends a POST request to /upload */
103     httpd_resp_send_chunk(req, (const char *)upload_script_start, upload_script_size);
104 
105     /* Send file-list table definition and column labels */
106     httpd_resp_sendstr_chunk(req,
107         "<table class=\"fixed\" border=\"1\">"
108         "<col width=\"800px\" /><col width=\"300px\" /><col width=\"300px\" /><col width=\"100px\" />"
109         "<thead><tr><th>Name</th><th>Type</th><th>Size (Bytes)</th><th>Delete</th></tr></thead>"
110         "<tbody>");
111 
112     /* Iterate over all files / folders and fetch their names and sizes */
113     while ((entry = readdir(dir)) != NULL) {
114         entrytype = (entry->d_type == DT_DIR ? "directory" : "file");
115 
116         strlcpy(entrypath + dirpath_len, entry->d_name, sizeof(entrypath) - dirpath_len);
117         if (stat(entrypath, &entry_stat) == -1) {
118             ESP_LOGE(TAG, "Failed to stat %s : %s", entrytype, entry->d_name);
119             continue;
120         }
121         sprintf(entrysize, "%ld", entry_stat.st_size);
122         ESP_LOGI(TAG, "Found %s : %s (%s bytes)", entrytype, entry->d_name, entrysize);
123 
124         /* Send chunk of HTML file containing table entries with file name and size */
125         httpd_resp_sendstr_chunk(req, "<tr><td><a href=\"");
126         httpd_resp_sendstr_chunk(req, req->uri);
127         httpd_resp_sendstr_chunk(req, entry->d_name);
128         if (entry->d_type == DT_DIR) {
129             httpd_resp_sendstr_chunk(req, "/");
130         }
131         httpd_resp_sendstr_chunk(req, "\">");
132         httpd_resp_sendstr_chunk(req, entry->d_name);
133         httpd_resp_sendstr_chunk(req, "</a></td><td>");
134         httpd_resp_sendstr_chunk(req, entrytype);
135         httpd_resp_sendstr_chunk(req, "</td><td>");
136         httpd_resp_sendstr_chunk(req, entrysize);
137         httpd_resp_sendstr_chunk(req, "</td><td>");
138         httpd_resp_sendstr_chunk(req, "<form method=\"post\" action=\"/delete");
139         httpd_resp_sendstr_chunk(req, req->uri);
140         httpd_resp_sendstr_chunk(req, entry->d_name);
141         httpd_resp_sendstr_chunk(req, "\"><button type=\"submit\">Delete</button></form>");
142         httpd_resp_sendstr_chunk(req, "</td></tr>\n");
143     }
144     closedir(dir);
145 
146     /* Finish the file list table */
147     httpd_resp_sendstr_chunk(req, "</tbody></table>");
148 
149     /* Send remaining chunk of HTML file to complete it */
150     httpd_resp_sendstr_chunk(req, "</body></html>");
151 
152     /* Send empty chunk to signal HTTP response completion */
153     httpd_resp_sendstr_chunk(req, NULL);
154     return ESP_OK;
155 }
156 
157 #define IS_FILE_EXT(filename, ext) \
158     (strcasecmp(&filename[strlen(filename) - sizeof(ext) + 1], ext) == 0)
159 
160 /* Set HTTP response content type according to file extension */
set_content_type_from_file(httpd_req_t * req,const char * filename)161 static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filename)
162 {
163     if (IS_FILE_EXT(filename, ".pdf")) {
164         return httpd_resp_set_type(req, "application/pdf");
165     } else if (IS_FILE_EXT(filename, ".html")) {
166         return httpd_resp_set_type(req, "text/html");
167     } else if (IS_FILE_EXT(filename, ".jpeg")) {
168         return httpd_resp_set_type(req, "image/jpeg");
169     } else if (IS_FILE_EXT(filename, ".ico")) {
170         return httpd_resp_set_type(req, "image/x-icon");
171     }
172     /* This is a limited set only */
173     /* For any other type always set as plain text */
174     return httpd_resp_set_type(req, "text/plain");
175 }
176 
177 /* Copies the full path into destination buffer and returns
178  * pointer to path (skipping the preceding base path) */
get_path_from_uri(char * dest,const char * base_path,const char * uri,size_t destsize)179 static const char* get_path_from_uri(char *dest, const char *base_path, const char *uri, size_t destsize)
180 {
181     const size_t base_pathlen = strlen(base_path);
182     size_t pathlen = strlen(uri);
183 
184     const char *quest = strchr(uri, '?');
185     if (quest) {
186         pathlen = MIN(pathlen, quest - uri);
187     }
188     const char *hash = strchr(uri, '#');
189     if (hash) {
190         pathlen = MIN(pathlen, hash - uri);
191     }
192 
193     if (base_pathlen + pathlen + 1 > destsize) {
194         /* Full path string won't fit into destination buffer */
195         return NULL;
196     }
197 
198     /* Construct full path (base + path) */
199     strcpy(dest, base_path);
200     strlcpy(dest + base_pathlen, uri, pathlen + 1);
201 
202     /* Return pointer to path, skipping the base */
203     return dest + base_pathlen;
204 }
205 
206 /* Handler to download a file kept on the server */
download_get_handler(httpd_req_t * req)207 static esp_err_t download_get_handler(httpd_req_t *req)
208 {
209     char filepath[FILE_PATH_MAX];
210     FILE *fd = NULL;
211     struct stat file_stat;
212 
213     const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path,
214                                              req->uri, sizeof(filepath));
215     if (!filename) {
216         ESP_LOGE(TAG, "Filename is too long");
217         /* Respond with 500 Internal Server Error */
218         httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long");
219         return ESP_FAIL;
220     }
221 
222     /* If name has trailing '/', respond with directory contents */
223     if (filename[strlen(filename) - 1] == '/') {
224         return http_resp_dir_html(req, filepath);
225     }
226 
227     if (stat(filepath, &file_stat) == -1) {
228         /* If file not present on SPIFFS check if URI
229          * corresponds to one of the hardcoded paths */
230         if (strcmp(filename, "/index.html") == 0) {
231             return index_html_get_handler(req);
232         } else if (strcmp(filename, "/favicon.ico") == 0) {
233             return favicon_get_handler(req);
234         }
235         ESP_LOGE(TAG, "Failed to stat file : %s", filepath);
236         /* Respond with 404 Not Found */
237         httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist");
238         return ESP_FAIL;
239     }
240 
241     fd = fopen(filepath, "r");
242     if (!fd) {
243         ESP_LOGE(TAG, "Failed to read existing file : %s", filepath);
244         /* Respond with 500 Internal Server Error */
245         httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file");
246         return ESP_FAIL;
247     }
248 
249     ESP_LOGI(TAG, "Sending file : %s (%ld bytes)...", filename, file_stat.st_size);
250     set_content_type_from_file(req, filename);
251 
252     /* Retrieve the pointer to scratch buffer for temporary storage */
253     char *chunk = ((struct file_server_data *)req->user_ctx)->scratch;
254     size_t chunksize;
255     do {
256         /* Read file in chunks into the scratch buffer */
257         chunksize = fread(chunk, 1, SCRATCH_BUFSIZE, fd);
258 
259         if (chunksize > 0) {
260             /* Send the buffer contents as HTTP response chunk */
261             if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) {
262                 fclose(fd);
263                 ESP_LOGE(TAG, "File sending failed!");
264                 /* Abort sending file */
265                 httpd_resp_sendstr_chunk(req, NULL);
266                 /* Respond with 500 Internal Server Error */
267                 httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
268                return ESP_FAIL;
269            }
270         }
271 
272         /* Keep looping till the whole file is sent */
273     } while (chunksize != 0);
274 
275     /* Close file after sending complete */
276     fclose(fd);
277     ESP_LOGI(TAG, "File sending complete");
278 
279     /* Respond with an empty chunk to signal HTTP response completion */
280 #ifdef CONFIG_EXAMPLE_HTTPD_CONN_CLOSE_HEADER
281     httpd_resp_set_hdr(req, "Connection", "close");
282 #endif
283     httpd_resp_send_chunk(req, NULL, 0);
284     return ESP_OK;
285 }
286 
287 /* Handler to upload a file onto the server */
upload_post_handler(httpd_req_t * req)288 static esp_err_t upload_post_handler(httpd_req_t *req)
289 {
290     char filepath[FILE_PATH_MAX];
291     FILE *fd = NULL;
292     struct stat file_stat;
293 
294     /* Skip leading "/upload" from URI to get filename */
295     /* Note sizeof() counts NULL termination hence the -1 */
296     const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path,
297                                              req->uri + sizeof("/upload") - 1, sizeof(filepath));
298     if (!filename) {
299         /* Respond with 500 Internal Server Error */
300         httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long");
301         return ESP_FAIL;
302     }
303 
304     /* Filename cannot have a trailing '/' */
305     if (filename[strlen(filename) - 1] == '/') {
306         ESP_LOGE(TAG, "Invalid filename : %s", filename);
307         httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Invalid filename");
308         return ESP_FAIL;
309     }
310 
311     if (stat(filepath, &file_stat) == 0) {
312         ESP_LOGE(TAG, "File already exists : %s", filepath);
313         /* Respond with 400 Bad Request */
314         httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "File already exists");
315         return ESP_FAIL;
316     }
317 
318     /* File cannot be larger than a limit */
319     if (req->content_len > MAX_FILE_SIZE) {
320         ESP_LOGE(TAG, "File too large : %d bytes", req->content_len);
321         /* Respond with 400 Bad Request */
322         httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
323                             "File size must be less than "
324                             MAX_FILE_SIZE_STR "!");
325         /* Return failure to close underlying connection else the
326          * incoming file content will keep the socket busy */
327         return ESP_FAIL;
328     }
329 
330     fd = fopen(filepath, "w");
331     if (!fd) {
332         ESP_LOGE(TAG, "Failed to create file : %s", filepath);
333         /* Respond with 500 Internal Server Error */
334         httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to create file");
335         return ESP_FAIL;
336     }
337 
338     ESP_LOGI(TAG, "Receiving file : %s...", filename);
339 
340     /* Retrieve the pointer to scratch buffer for temporary storage */
341     char *buf = ((struct file_server_data *)req->user_ctx)->scratch;
342     int received;
343 
344     /* Content length of the request gives
345      * the size of the file being uploaded */
346     int remaining = req->content_len;
347 
348     while (remaining > 0) {
349 
350         ESP_LOGI(TAG, "Remaining size : %d", remaining);
351         /* Receive the file part by part into a buffer */
352         if ((received = httpd_req_recv(req, buf, MIN(remaining, SCRATCH_BUFSIZE))) <= 0) {
353             if (received == HTTPD_SOCK_ERR_TIMEOUT) {
354                 /* Retry if timeout occurred */
355                 continue;
356             }
357 
358             /* In case of unrecoverable error,
359              * close and delete the unfinished file*/
360             fclose(fd);
361             unlink(filepath);
362 
363             ESP_LOGE(TAG, "File reception failed!");
364             /* Respond with 500 Internal Server Error */
365             httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file");
366             return ESP_FAIL;
367         }
368 
369         /* Write buffer content to file on storage */
370         if (received && (received != fwrite(buf, 1, received, fd))) {
371             /* Couldn't write everything to file!
372              * Storage may be full? */
373             fclose(fd);
374             unlink(filepath);
375 
376             ESP_LOGE(TAG, "File write failed!");
377             /* Respond with 500 Internal Server Error */
378             httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to write file to storage");
379             return ESP_FAIL;
380         }
381 
382         /* Keep track of remaining size of
383          * the file left to be uploaded */
384         remaining -= received;
385     }
386 
387     /* Close file upon upload completion */
388     fclose(fd);
389     ESP_LOGI(TAG, "File reception complete");
390 
391     /* Redirect onto root to see the updated file list */
392     httpd_resp_set_status(req, "303 See Other");
393     httpd_resp_set_hdr(req, "Location", "/");
394 #ifdef CONFIG_EXAMPLE_HTTPD_CONN_CLOSE_HEADER
395     httpd_resp_set_hdr(req, "Connection", "close");
396 #endif
397     httpd_resp_sendstr(req, "File uploaded successfully");
398     return ESP_OK;
399 }
400 
401 /* Handler to delete a file from the server */
delete_post_handler(httpd_req_t * req)402 static esp_err_t delete_post_handler(httpd_req_t *req)
403 {
404     char filepath[FILE_PATH_MAX];
405     struct stat file_stat;
406 
407     /* Skip leading "/delete" from URI to get filename */
408     /* Note sizeof() counts NULL termination hence the -1 */
409     const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path,
410                                              req->uri  + sizeof("/delete") - 1, sizeof(filepath));
411     if (!filename) {
412         /* Respond with 500 Internal Server Error */
413         httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long");
414         return ESP_FAIL;
415     }
416 
417     /* Filename cannot have a trailing '/' */
418     if (filename[strlen(filename) - 1] == '/') {
419         ESP_LOGE(TAG, "Invalid filename : %s", filename);
420         httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Invalid filename");
421         return ESP_FAIL;
422     }
423 
424     if (stat(filepath, &file_stat) == -1) {
425         ESP_LOGE(TAG, "File does not exist : %s", filename);
426         /* Respond with 400 Bad Request */
427         httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "File does not exist");
428         return ESP_FAIL;
429     }
430 
431     ESP_LOGI(TAG, "Deleting file : %s", filename);
432     /* Delete file */
433     unlink(filepath);
434 
435     /* Redirect onto root to see the updated file list */
436     httpd_resp_set_status(req, "303 See Other");
437     httpd_resp_set_hdr(req, "Location", "/");
438 #ifdef CONFIG_EXAMPLE_HTTPD_CONN_CLOSE_HEADER
439     httpd_resp_set_hdr(req, "Connection", "close");
440 #endif
441     httpd_resp_sendstr(req, "File deleted successfully");
442     return ESP_OK;
443 }
444 
445 /* Function to start the file server */
start_file_server(const char * base_path)446 esp_err_t start_file_server(const char *base_path)
447 {
448     static struct file_server_data *server_data = NULL;
449 
450     /* Validate file storage base path */
451     if (!base_path || strcmp(base_path, "/spiffs") != 0) {
452         ESP_LOGE(TAG, "File server presently supports only '/spiffs' as base path");
453         return ESP_ERR_INVALID_ARG;
454     }
455 
456     if (server_data) {
457         ESP_LOGE(TAG, "File server already started");
458         return ESP_ERR_INVALID_STATE;
459     }
460 
461     /* Allocate memory for server data */
462     server_data = calloc(1, sizeof(struct file_server_data));
463     if (!server_data) {
464         ESP_LOGE(TAG, "Failed to allocate memory for server data");
465         return ESP_ERR_NO_MEM;
466     }
467     strlcpy(server_data->base_path, base_path,
468             sizeof(server_data->base_path));
469 
470     httpd_handle_t server = NULL;
471     httpd_config_t config = HTTPD_DEFAULT_CONFIG();
472 
473     /* Use the URI wildcard matching function in order to
474      * allow the same handler to respond to multiple different
475      * target URIs which match the wildcard scheme */
476     config.uri_match_fn = httpd_uri_match_wildcard;
477 
478     ESP_LOGI(TAG, "Starting HTTP Server on port: '%d'", config.server_port);
479     if (httpd_start(&server, &config) != ESP_OK) {
480         ESP_LOGE(TAG, "Failed to start file server!");
481         return ESP_FAIL;
482     }
483 
484     /* URI handler for getting uploaded files */
485     httpd_uri_t file_download = {
486         .uri       = "/*",  // Match all URIs of type /path/to/file
487         .method    = HTTP_GET,
488         .handler   = download_get_handler,
489         .user_ctx  = server_data    // Pass server data as context
490     };
491     httpd_register_uri_handler(server, &file_download);
492 
493     /* URI handler for uploading files to server */
494     httpd_uri_t file_upload = {
495         .uri       = "/upload/*",   // Match all URIs of type /upload/path/to/file
496         .method    = HTTP_POST,
497         .handler   = upload_post_handler,
498         .user_ctx  = server_data    // Pass server data as context
499     };
500     httpd_register_uri_handler(server, &file_upload);
501 
502     /* URI handler for deleting files from server */
503     httpd_uri_t file_delete = {
504         .uri       = "/delete/*",   // Match all URIs of type /delete/path/to/file
505         .method    = HTTP_POST,
506         .handler   = delete_post_handler,
507         .user_ctx  = server_data    // Pass server data as context
508     };
509     httpd_register_uri_handler(server, &file_delete);
510 
511     return ESP_OK;
512 }
513