lathoub / express

Fast, unopinionated, (very) minimalist web framework for Arduino ESP32
GNU General Public License v3.0
1 stars 1 forks source link

Send file example? [ QUESTION ] #7

Open zekageri opened 1 year ago

zekageri commented 1 year ago
app.get("/", [](request &req, response &res,const NextCallback next) {
    //res.send(F("Hello World!"));
    res.sendFile();
});

??

zekageri commented 1 year ago

I'm thinking about something like this

#include <LittleFS.h>

auto _Response::sendFile(const char* filePath) -> void {
  if( !LittleFS.exists(filePath) ){ return; }
  fs::File file = LittleFS.open(filePath, "r");
  if(!file){ return; }
  while(file.available()){
    // read file to buffer for sending or send it byte by byte?
  }
  file.close();
}
zekageri commented 1 year ago

Or should we just implement it in user side? I created a quick draft, something like this:

#include <LittleFS.h>

char buffer[5000];
class index {
    public:
        static constexpr char *filename = "/index.html";
        static const char *content() {
            int size = fileSys.readFile("/index.html",buffer,5000);
            Serial.printf("Read file size: %d\n", size);
            Serial.println(buffer);
            return buffer;
        }
};

app.get("/", [](request &req, response &res,const NextCallback next) {
    express_::File file{"index.html", index::content};
    res.sendFile(file);
});

It's an ugly code but works.

zekageri commented 1 year ago

Or something like this

const int expressFileSize = 5000; // should be dinamyc depending on the file size.
char expressFileBuffer[expressFileSize];
char* expressFilePath;

const char *expressFileCB() {
    int size = fileSys.getFileSize(expressFilePath);
    if( !size ){
        fileSys.readFile("/notFound.html",expressFileBuffer,expressFileSize);
    }else{
        fileSys.readFile(expressFilePath,expressFileBuffer,expressFileSize);
    }
    return expressFileBuffer;
}

express_::File createExpressFile( const char* path ){
    expressFilePath = (char*)path;
    express_::File file{path, expressFileCB};
    return file;
}

app.get("/", [](request &req, response &res,const NextCallback next) {
    express_::File file = createExpressFile("/index.html");
    res.sendFile(file);
});
lathoub commented 1 year ago

See #8, what file system to use? (use seem to use LittleFS) - or how to choose one at compile time (eg through the use of C++ templates)

zekageri commented 1 year ago

Offical new file system for esp32 is LittleFS but it could accept a standard File as an input. Look at Me_No_Dev's ESPAsyncWebServer implementation

zekageri commented 1 year ago

AsyncWebserver does this:

AsyncWebServerResponse *response =  request->beginResponse(
    LittleFS,
    MAIN_PATH,
    "text/html"
);
//response->addHeader("Set-Cookie", cookie);
//response->addHeader("Content-Encoding", "gzip");
request->send(response);

You can pass in the file system.

ESPAsyncWebServer.h

AsyncWebServerResponse *beginResponse(FS &fs, const String& path, const String& contentType=String(), bool download=false, AwsTemplateProcessor callback=nullptr);

WebRequest.cpp

AsyncWebServerResponse * AsyncWebServerRequest::beginResponse(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback){
  if(fs.exists(path) || (!download && fs.exists(path+".gz")))
    return new AsyncFileResponse(fs, path, contentType, download, callback);
  return NULL;
}

WebResponses.cpp

AsyncFileResponse::AsyncFileResponse(FS &fs, const String& path, const String& contentType, bool download, AwsTemplateProcessor callback): AsyncAbstractResponse(callback){
  _code = 200;
  _path = path;

  if(!download && !fs.exists(_path) && fs.exists(_path+".gz")){
    _path = _path+".gz";
    addHeader("Content-Encoding", "gzip");
    _callback = nullptr; // Unable to process zipped templates
    _sendContentLength = true;
    _chunked = false;
  }

  _content = fs.open(_path, "r");
  _contentLength = _content.size();

  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);
}

So it's just pushes the file system too.

zekageri commented 1 year ago

I'm thiking about something like this:

/**
 * @brief Send the contents of a file from the file system as the response.
 *
 * @param filePath The path of the file to send.
 */
auto _Response::sendFile(FS &fs, const char *filePath) -> void {
    // Open the file from LittleFS
    fs::File file = fs.open(filePath);

    // Check if the file was successfully opened
    if (file) {
        // Get the file size
        size_t fileSize = file.size();

        // Allocate a buffer for the file contents
        char *buffer = new char[fileSize + 1];

        // Read the file contents into the buffer
        file.readBytes(buffer, fileSize);

        // Close the file
        file.close();

        // Set the response headers
        this->set(F("Content-Length"), String(fileSize));
        this->status(HttpStatus::OK);

        // Send the file contents as the response body
        this->send(buffer);

        // Free the buffer
        delete[] buffer;
    } else {
        // Failed to open the file, set appropriate response status
        this->status(HttpStatus::NOT_FOUND);
    }
}
zekageri commented 1 year ago

We could use it like this

app.get("/", [](request &req, response &res,const NextCallback next) {
    res.sendFile(LittleFS,"/index.html");
});
zekageri commented 1 year ago
/**
 * @brief Send the contents of a file from the file system as the response.
 *
 * @param filePath The path of the file to send.
 */
auto _Response::sendFile(FS &fs, const char *filePath) -> void {
    fs::File file = fs.open(filePath);
    if (file) {
        size_t fileSize = file.size();

        // Allocate a buffer with file size + 1
        char *buffer = new char[fileSize + 1];

        // Read the file contents directly into the buffer
        file.readBytes(buffer, fileSize);

        // Close the file
        file.close();

        // Set the response headers
        this->set(ContentType, F("text/html"));
        this->set(ContentLength, String(fileSize));
        status(HttpStatus::OK);

        body_ = buffer;

        delete[] buffer;
    } else {
        status(HttpStatus::NOT_FOUND);
    }
}

This way i can send a file from the file system but it is a little funky looking. Maybe because a character encoding header is missing? I will check this out

image image

zekageri commented 1 year ago

So i forgot to add a null terminator. It works like this now

/**
 * @brief Send the contents of a file from the file system as the response.
 *
 * @param filePath The path of the file to send.
 */
auto _Response::sendFile(FS &fs, const char *filePath) -> void {
    fs::File file = fs.open(filePath);
    if (file) {
        size_t fileSize = file.size();

        // Allocate a buffer with file size + 1
        char *buffer = new char[fileSize + 1];

        // Read the file contents directly into the buffer
        file.readBytes(buffer, fileSize);

        // Close the file
        file.close();

        // Set the response headers
        this->set(ContentType, mimeType.getType(filePath)); // I have added a library to automatically handle mime types
        this->set(ContentLength, String(fileSize+1));
        status(HttpStatus::OK);

        // Add null terminator at the end of the buffer
        buffer[fileSize] = '\0';

        body_ = buffer;

        delete[] buffer;
    } else {
        status(HttpStatus::NOT_FOUND);
    }
}
zekageri commented 1 year ago

Yeah, so it doesn't handle large files well. We have to send it chunked from the file system no matter what.

lathoub commented 1 year ago

The library has been designed not to copy the input buffer (but use pointers over the input buffer) - not sure what is going on, but it could be that at the data pointer from the FS is not behaving well.

Context: I try to avoid new/delete to avoid memory fragmentation on the microcontroller.

zekageri commented 1 year ago

Yeah it would be cool if we could pull chunks directly from the file without copying all its content to a buffer