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