Skip to content

Instantly share code, notes, and snippets.

@pim-borst
Last active June 10, 2023 03:09
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save pim-borst/17934bfd4454caea3ba4f74366c2135c to your computer and use it in GitHub Desktop.
Save pim-borst/17934bfd4454caea3ba4f74366c2135c to your computer and use it in GitHub Desktop.
Serve files from an SD-card using Me No Dev's async webserver for the ESP8266.
/*
* Example how to serve files from an SD-card using Me No Dev's async webserver for the ESP8266.
* See https://github.com/me-no-dev/ESPAsyncWebServer
*
* Basically I copied the code for serving static files from SPIFFS and modified it for the SD library.
* To resolve conflicts between SPIFFS and SD File classes I had to include the sd namespace in SD.h and SD.cpp in packages/esp8266/hardware/esp8266/2.3.0/libraries/SD/src.
* So in SD.h put "namespace sd { ... }; using namespace sd;" around everything excluding the # preprocessor directives.
* And in SD.cpp put "namespace sd { ... };" around everything excluding the # preprocessor directives.
*
* Also, don't forget to fill in your WiFi SSID and password and put an index.htm file on your SD card.
*/
#include <ESP8266WiFi.h>
/* #include <FS.h> */
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
AsyncWebServer server(80);
const char* hostName = "asyncsd";
const char* ssid = "******";
const char* password = "********";
const char* sdFile = "/index.htm";
#include "SPI.h"
#include "SD.h"
bool SD_exists(SDClass &sd, String path) {
// For some reason SD.exists(filename) reboots the ESP...
// So we test by opening the file
bool exists = false;
sd::File test = sd.open(path);
if(test){
test.close();
exists = true;
}
return exists;
}
class AsyncSDFileResponse: public AsyncAbstractResponse {
private:
sd::File _content;
String _path;
void _setContentType(const String& path);
bool _sourceIsValid;
public:
AsyncSDFileResponse(SDClass &sd, const String& path, const String& contentType=String(), bool download=false);
AsyncSDFileResponse(sd::File content, const String& path, const String& contentType=String(), bool download=false);
~AsyncSDFileResponse();
bool _sourceValid() const { return _sourceIsValid; }
virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override;
};
AsyncSDFileResponse::~AsyncSDFileResponse(){
if(_content)
_content.close();
}
void AsyncSDFileResponse::_setContentType(const String& path){
if (path.endsWith(".html")) _contentType = "text/html";
else if (path.endsWith(".htm")) _contentType = "text/html";
else if (path.endsWith(".css")) _contentType = "text/css";
else if (path.endsWith(".json")) _contentType = "text/json";
else if (path.endsWith(".js")) _contentType = "application/javascript";
else if (path.endsWith(".png")) _contentType = "image/png";
else if (path.endsWith(".gif")) _contentType = "image/gif";
else if (path.endsWith(".jpg")) _contentType = "image/jpeg";
else if (path.endsWith(".ico")) _contentType = "image/x-icon";
else if (path.endsWith(".svg")) _contentType = "image/svg+xml";
else if (path.endsWith(".eot")) _contentType = "font/eot";
else if (path.endsWith(".woff")) _contentType = "font/woff";
else if (path.endsWith(".woff2")) _contentType = "font/woff2";
else if (path.endsWith(".ttf")) _contentType = "font/ttf";
else if (path.endsWith(".xml")) _contentType = "text/xml";
else if (path.endsWith(".pdf")) _contentType = "application/pdf";
else if (path.endsWith(".zip")) _contentType = "application/zip";
else if(path.endsWith(".gz")) _contentType = "application/x-gzip";
else _contentType = "text/plain";
}
AsyncSDFileResponse::AsyncSDFileResponse(SDClass &sd, const String& path, const String& contentType, bool download){
_code = 200;
_path = path;
if(!download && !SD_exists(sd, _path) && SD_exists(sd, _path+".gz")){
_path = _path+".gz";
addHeader("Content-Encoding", "gzip");
}
_content = sd.open(_path, FILE_READ);
_contentLength = _content.size();
_sourceIsValid = _content;
if(contentType == "")
_setContentType(path);
else
_contentType = contentType;
int filenameStart = path.lastIndexOf('/') + 1;
char buf[26+path.length()-filenameStart];
char* filename = (char*)path.c_str() + filenameStart;
if(download) {
// set filename and force download
snprintf(buf, sizeof (buf), "attachment; filename=\"%s\"", filename);
} else {
// set filename and force rendering
snprintf(buf, sizeof (buf), "inline; filename=\"%s\"", filename);
}
addHeader("Content-Disposition", buf);
}
AsyncSDFileResponse::AsyncSDFileResponse(sd::File content, const String& path, const String& contentType, bool download){
_code = 200;
_path = path;
_content = content;
_contentLength = _content.size();
if(!download && String(_content.name()).endsWith(".gz") && !path.endsWith(".gz"))
addHeader("Content-Encoding", "gzip");
if(contentType == "")
_setContentType(path);
else
_contentType = contentType;
int filenameStart = path.lastIndexOf('/') + 1;
char buf[26+path.length()-filenameStart];
char* filename = (char*)path.c_str() + filenameStart;
if(download) {
snprintf(buf, sizeof (buf), "attachment; filename=\"%s\"", filename);
} else {
snprintf(buf, sizeof (buf), "inline; filename=\"%s\"", filename);
}
addHeader("Content-Disposition", buf);
}
size_t AsyncSDFileResponse::_fillBuffer(uint8_t *data, size_t len){
_content.read(data, len);
return len;
}
class AsyncStaticSDWebHandler: public AsyncWebHandler {
private:
bool _getFile(AsyncWebServerRequest *request);
bool _fileExists(AsyncWebServerRequest *request, const String& path);
uint8_t _countBits(const uint8_t value) const;
protected:
SDClass _sd;
String _uri;
String _path;
String _default_file;
String _cache_control;
String _last_modified;
bool _isDir;
bool _gzipFirst;
uint8_t _gzipStats;
public:
AsyncStaticSDWebHandler(const char* uri, SDClass& sd, const char* path, const char* cache_control = NULL);
virtual bool canHandle(AsyncWebServerRequest *request) override final;
virtual void handleRequest(AsyncWebServerRequest *request) override final;
AsyncStaticSDWebHandler& setIsDir(bool isDir);
AsyncStaticSDWebHandler& setDefaultFile(const char* filename);
AsyncStaticSDWebHandler& setCacheControl(const char* cache_control);
AsyncStaticSDWebHandler& setLastModified(const char* last_modified);
AsyncStaticSDWebHandler& setLastModified(struct tm* last_modified);
#ifdef ESP8266
AsyncStaticSDWebHandler& setLastModified(time_t last_modified);
AsyncStaticSDWebHandler& setLastModified(); //sets to current time. Make sure sntp is runing and time is updated
#endif
};
AsyncStaticSDWebHandler::AsyncStaticSDWebHandler(const char* uri, SDClass& sd, const char* path, const char* cache_control)
: _sd(sd), _uri(uri), _path(path), _default_file("index.htm"), _cache_control(cache_control), _last_modified("")
{
// Ensure leading '/'
if (_uri.length() == 0 || _uri[0] != '/') _uri = "/" + _uri;
if (_path.length() == 0 || _path[0] != '/') _path = "/" + _path;
// If path ends with '/' we assume a hint that this is a directory to improve performance.
// However - if it does not end with '/' we, can't assume a file, path can still be a directory.
_isDir = _path[_path.length()-1] == '/';
// Remove the trailing '/' so we can handle default file
// Notice that root will be "" not "/"
if (_uri[_uri.length()-1] == '/') _uri = _uri.substring(0, _uri.length()-1);
if (_path[_path.length()-1] == '/') _path = _path.substring(0, _path.length()-1);
// Reset stats
_gzipFirst = false;
_gzipStats = 0xF8;
}
AsyncStaticSDWebHandler& AsyncStaticSDWebHandler::setIsDir(bool isDir){
_isDir = isDir;
return *this;
}
AsyncStaticSDWebHandler& AsyncStaticSDWebHandler::setDefaultFile(const char* filename){
_default_file = String(filename);
return *this;
}
AsyncStaticSDWebHandler& AsyncStaticSDWebHandler::setCacheControl(const char* cache_control){
_cache_control = String(cache_control);
return *this;
}
AsyncStaticSDWebHandler& AsyncStaticSDWebHandler::setLastModified(const char* last_modified){
_last_modified = String(last_modified);
return *this;
}
AsyncStaticSDWebHandler& AsyncStaticSDWebHandler::setLastModified(struct tm* last_modified){
char result[30];
strftime (result,30,"%a, %d %b %Y %H:%M:%S %Z", last_modified);
return setLastModified((const char *)result);
}
#ifdef ESP8266
AsyncStaticSDWebHandler& AsyncStaticSDWebHandler::setLastModified(time_t last_modified){
return setLastModified((struct tm *)gmtime(&last_modified));
}
AsyncStaticSDWebHandler& AsyncStaticSDWebHandler::setLastModified(){
time_t last_modified;
if(time(&last_modified) == 0) //time is not yet set
return *this;
return setLastModified(last_modified);
}
#endif
bool AsyncStaticSDWebHandler::canHandle(AsyncWebServerRequest *request){
if (request->method() == HTTP_GET &&
request->url().startsWith(_uri) &&
_getFile(request)) {
// We interested in "If-Modified-Since" header to check if file was modified
if (_last_modified.length())
request->addInterestingHeader("If-Modified-Since");
if(_cache_control.length())
request->addInterestingHeader("If-None-Match");
DEBUGF("[AsyncStaticSDWebHandler::canHandle] TRUE\n");
return true;
}
return false;
}
bool AsyncStaticSDWebHandler::_getFile(AsyncWebServerRequest *request)
{
// Remove the found uri
String path = request->url().substring(_uri.length());
// We can skip the file check and look for default if request is to the root of a directory or that request path ends with '/'
bool canSkipFileCheck = (_isDir && path.length() == 0) || (path.length() && path[path.length()-1] == '/');
path = _path + path;
// Do we have a file or .gz file
if (!canSkipFileCheck && _fileExists(request, path))
return true;
// Can't handle if not default file
if (_default_file.length() == 0)
return false;
// Try to add default file, ensure there is a trailing '/' ot the path.
if (path.length() == 0 || path[path.length()-1] != '/')
path += "/";
path += _default_file;
return _fileExists(request, path);
}
bool AsyncStaticSDWebHandler::_fileExists(AsyncWebServerRequest *request, const String& path)
{
bool fileFound = false;
bool gzipFound = false;
String gzip = path + ".gz";
// Following part reworked to use SD_exists instead of request->_tempFile = _fs.open()
// Drawback: AsyncSDFileResponse(sd::File content, ...) cannot be used so
// file needs to be opened again in AsyncSDFileResponse(SDClass &sd, ...)
// request->_tempFile is of wrong fs::File type anyway...
if (_gzipFirst) {
gzipFound = SD_exists(_sd, gzip);
if (!gzipFound){
fileFound = SD_exists(_sd, path);
}
} else {
fileFound = SD_exists(_sd, path);
if (!fileFound){
gzipFound = SD_exists(_sd, gzip);
}
}
bool found = fileFound || gzipFound;
if (found) {
// Extract the file name from the path and keep it in _tempObject
size_t pathLen = path.length();
char * _tempPath = (char*)malloc(pathLen+1);
snprintf(_tempPath, pathLen+1, "%s", path.c_str());
request->_tempObject = (void*)_tempPath;
// Calculate gzip statistic
_gzipStats = (_gzipStats << 1) + (gzipFound ? 1 : 0);
if (_gzipStats == 0x00) _gzipFirst = false; // All files are not gzip
else if (_gzipStats == 0xFF) _gzipFirst = true; // All files are gzip
else _gzipFirst = _countBits(_gzipStats) > 4; // IF we have more gzip files - try gzip first
}
return found;
}
uint8_t AsyncStaticSDWebHandler::_countBits(const uint8_t value) const
{
uint8_t w = value;
uint8_t n;
for (n=0; w!=0; n++) w&=w-1;
return n;
}
void AsyncStaticSDWebHandler::handleRequest(AsyncWebServerRequest *request)
{
// Get the filename from request->_tempObject and free it
String filename = String((char*)request->_tempObject);
free(request->_tempObject);
request->_tempObject = NULL;
sd::File _tempFile = _sd.open(filename);
if (_tempFile == true) {
String etag = String(_tempFile.size());
_tempFile.close();
if (_last_modified.length() && _last_modified == request->header("If-Modified-Since")) {
request->send(304); // Not modified
} else if (_cache_control.length() && request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag)) {
AsyncWebServerResponse * response = new AsyncBasicResponse(304); // Not modified
response->addHeader("Cache-Control", _cache_control);
response->addHeader("ETag", etag);
request->send(response);
} else {
// Cannot use new AsyncSDFileResponse(request->_tempFile, ...) here because request->_tempFile has not be opened
// in AsyncStaticSDWebHandler::_fileExists and is of wrong type fs::File anyway.
AsyncWebServerResponse * response = new AsyncSDFileResponse(/* request->_tempFile, */ _sd, filename);
if (_last_modified.length())
response->addHeader("Last-Modified", _last_modified);
if (_cache_control.length()){
response->addHeader("Cache-Control", _cache_control);
response->addHeader("ETag", etag);
}
request->send(response);
}
} else {
request->send(404);
}
}
/*
* We don't need this one
AsyncStaticSDWebHandler& AsyncWebServer::serveStatic(const char* uri, SDClass& sd, const char* path, const char* cache_control){
AsyncStaticSDWebHandler* handler = new AsyncStaticSDWebHandler(uri, sd, path, cache_control);
addHandler(handler);
return *handler;
}
*/
extern "C" void system_set_os_print(uint8 onoff);
extern "C" void ets_install_putc1(void* routine);
static void _u0_putc(char c){
while(((U0S >> USTXC) & 0x7F) != 0);
U0F = c;
}
void initSerial(){
Serial.begin(115200);
ets_install_putc1((void *) &_u0_putc);
system_set_os_print(1);
}
void initWiFi(){
WiFi.hostname(hostName);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
os_printf("STA: Failed!\n");
}
}
bool initSD(){
if (SD.begin(SS, 32000000)){
sd::File entry;
sd::File dir = SD.open("/");
dir.rewindDirectory();
while(true){
entry = dir.openNextFile();
if (!entry) break;
Serial.printf("SD File: %s, type: %s, size: %ld\n", entry.name(), (entry.isDirectory())?"dir":"file", entry.size());
entry.close();
}
dir.close();
Serial.println();
return true;
} else
Serial.println("SD Card Not Found!");
return false;
}
void setup(){
initSerial();
// SPIFFS.begin();
initWiFi();
initSD();
server.addHandler(new AsyncStaticSDWebHandler(sdFile, SD, sdFile));
server.onNotFound([](AsyncWebServerRequest *request){
os_printf("NOT_FOUND: ");
if(request->method() == HTTP_GET)
os_printf("GET");
else if(request->method() == HTTP_POST)
os_printf("POST");
else if(request->method() == HTTP_DELETE)
os_printf("DELETE");
else if(request->method() == HTTP_PUT)
os_printf("PUT");
else if(request->method() == HTTP_PATCH)
os_printf("PATCH");
else if(request->method() == HTTP_HEAD)
os_printf("HEAD");
else if(request->method() == HTTP_OPTIONS)
os_printf("OPTIONS");
else
os_printf("UNKNOWN");
os_printf(" http://%s%s\n", request->host().c_str(), request->url().c_str());
request->send(404);
});
server.begin();
}
static uint32_t lastTest = 0;
void loop(){
uint32_t newTest = ESP.getFreeHeap();
if(newTest != lastTest){
lastTest = newTest;
os_printf("%u\r\n", newTest);
}
}
@samuk
Copy link

samuk commented Nov 16, 2019

Hi could you provide a licence for this code? It's being used here: https://github.com/paidforby/AsyncSDServer and would be good to be able to add a licence.

@frnck37
Copy link

frnck37 commented Jan 19, 2023

Vry good ! i've been struggling with this for days !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment