me-no-dev / ESPAsyncWebServer

Async Web Server for ESP8266 and ESP32
3.75k stars 1.22k forks source link

AsyncWebServer download SPIFFS file using url() #627

Closed Tech500 closed 5 years ago

Tech500 commented 5 years ago

Code displays file instead of prompting to save file:

// Serve files in directory "/" when request url starts with "/" // Request to the root or none existing files will try to server the defualt // file name "index.htm" if exists serverAsync.serveStatic("/", SPIFFS, "/");

I have a list of files on a web page that are URL's of the filenames. Looking for a way to prompt for save as/open choice or at least downloading selected URL (filename.)

Pablo2048 commented 5 years ago

Hi William, you can try to use html 5 'download' attribute in your html.

Tech500 commented 5 years ago

Working with HTML 5 download attribute I am not having any success.

What is the recommended way to download a file when URL is selected; using AsyncWebServer?

Pablo2048 commented 5 years ago

According to my tests it's not problem of AsyncWebServer, but browser and/or content type. Suggestion: use the download attribute and rename file exension from txt to text - tested on firefox and it is working as expected.

Tech500 commented 5 years ago

Could it be a path issue for the download attribute? I have another download link (Server: README) that works correctly.it downloads a specific file and does not use ULR list of files

'Server: README

What does the path look like that you used in your test?



    File root = SPIFFS.open("/");

    File file = root.openNextFile();

    while (file)
    {

       if(strncmp(file.name(), "/LOG", 4) == 0)
       {
            str += "<a href=\"";
            str += file.name();                                         
            str += "\">";
            str += file.name();
            str += "</a>"; 
            str += "    ";
            str += file.size();
            str += "<br>\r\n";

       }

       file = root.openNextFile();
    }

I have tried different path variables, place holders and var's/  My last attempt code;

``<a href=http://file.name()/file.name() download>

Also tried <a href=http://%PUBLICIP%/%FILENAME% download>

All attempts were placed in "HTLM2.h" that is all html.  

Which "server.on" did you use in your tests?

William
Pablo2048 commented 5 years ago

I'm testing all in pure HTML on computer using just the browser to identify the source of the problem. Actually I'm using two files:

<!DOCTYPE html>
<html>
<body>

<p>Click on the w3schools logo to download the image:<p>

<a href="will.text" download>Download test
</a>

<p><b>Note:</b> The download attribute is not supported in Edge version 12, IE, Safari 10 (and earlier), or Opera version 12 (and earlier).</p>

</body>
</html>

named as will.htm and

Just testing download...

named as will.text. After this two files are placed in directory, try to open the htm one in the browser and click on "Download test" - the download dialog should pop-up.

Tech500 commented 5 years ago

I am using this code in the HTML:

<a download href=http:///%PUBLICIP%/%FILENAME%>

and in setup:

``serverAsync.serveStatic("/", SPIFFS, "/"); Does this code call HTML2 and processor2 function?

I get "Page is not working." Chrome browser, I do "More tools -> Save as" I get download window and the filename is correct.

How do I get the download window when I click on the URL of the file name list?

atanisoft commented 5 years ago

you can try adding the header "Content-Disposition" with value of "attachment" on the server side, this should trigger the browser to treat it as a download rather than render in the browser.

Tech500 commented 5 years ago

atanisoft

Please, could provide header to add?

William

atanisoft commented 5 years ago

The header name is "Content-Disposition" and value of the header is "attachment"

Tech500 commented 5 years ago

I am just starting with AsyncWebServe.

Could give more complete detail on "adding header," please?

atanisoft commented 5 years ago

The readme has a lot of examples that might help you get started, here is an example showing how to serve the file with the extra header: https://github.com/me-no-dev/ESPAsyncWebServer/blob/master/README.md#respond-with-content-coming-from-a-file-and-extra-headers

Tech500 commented 5 years ago

atanisoft

Read the section you linked to; no complete, usage example. No progress.

Trying this code:

``serverAsync.serveStatic("/", SPIFFS, "/"); AsyncWebServerResponse *response = request->beginResponse(SPIFFS, "/", String(), true); response->addHeader("Content-Disposition", "attachment"); request->send(response);

Produces: 'request' was not declared in this scope

atanisoft commented 5 years ago

That is an abstract example snippet, you would need to put that inside a handler for a specific URI and not as a general endpoint for / via serveStatic.

Pablo2048 commented 5 years ago

@Tech500 you can not do it this way. If you want to follow the way @atanisoft suggest, then you have to remove .serveStatic, write your own onNotFound handler, which serve the file from SPIFFS. But if you do it this way, then it is useless to add Content-disposition header - use the "download" bool argument from https://github.com/me-no-dev/ESPAsyncWebServer/blob/master/src/ESPAsyncWebServer.h#L238 ...

Tech500 commented 5 years ago

I do not have the expertise to write own onNotFound handler that is needed. Will google to see if I can find an example for own onNotFound handler.

Appreciate you both for taking time to help me.

Tech500 commented 5 years ago

Closer to working:

Found tutorial: https://techtutorialsx.com/2019/03/29/esp32-arduino-serving-file-as-attachment/ and using this code:

serverAsync.on(filename, HTTP_GET, [](AsyncWebServerRequest request){ AsyncWebServerResponse response = request->beginResponse(200, "text/plain", "Ok"); response->addHeader("Content-Disposition", "attachment"); request->send(response); });

Produces download window to save file, has correct filename; however, only data in the file is "ok" with no additional content and a size of 1 KB. Actual file size is: 18928.

Any comment?

Pablo2048 commented 5 years ago

William, my comment is that this piece code does what it has to do - just start sending data with Ok text inside. As I said before - you have to:

  1. remove serverAsync.serveStatic() because you dont want to serve any file automatically (I don't like doing this, because I think that using donwload attribute is better way, but for beginner like you it seems to be the simplest method)
  2. write new method for not found handler - I've write some skeleton for you:

    void notFound(AsyncWebServerRequest *request) {
    
    if (request->url().endsWith(F(".txt"))) {
    // here comes some mambo-jambo to extract the filename from request->url()
    
    // ... and finally
    request->send(SPIFFS, <extracted filename>, String(), true);
    } else {
    request->send_P(404, PSTR("text/plain"), PSTR("Not found"));
    }
    }

    you have to write some code to extract filename with path from given url (mentioned in code)

  3. set web server not found handler:
    serverAsync.onNotFound(notFound);

    Pavel

Tech500 commented 5 years ago

Code from: []https://esp32.com/viewtopic.php?t=11307(url):

` if (request->hasParam("filename", true)) { // Check for files: <input name="filename" /> if (request->hasArg("download")) { // Download file: <button name="download"></button> Serial.println("Download Filename: " + request->arg("filename")); AsyncWebServerResponse *response = request->beginResponse(SPIFFS, request->arg("filename"), String(), true); response->addHeader("Server", "ESP Async Web Server"); request->send(response); return; } else if(request->hasArg("delete")) { // Delete file if (SPIFFS.remove(request->getParam("filename", true)->value())) { logOutput((String)request->getParam("filename", true)->value().c_str() + " was deleted !"); } else { logOutput("Could not delete file. Try again !"); } request->redirect("/files"); } }

Could this be modified to work for the "new method?"

This is the code from previous web server that downloaded the selected url:

``else if ((strncmp(path, "/LOG", 4) == 0) || (strcmp(path, "/README.TXT") == 0)) // Respond with the path that was accessed. {

char *filename; strcpy( MyBuffer, path ); filename = &MyBuffer[1];

if ((strcmp(path, "/FAVICON.ICO") == 0) || (strncmp(path, "/SYSTEM~1", 9) == 0) || (strncmp(path, "/ACCESS", 7) == 0)) {

client.println("HTTP/1.1 404 Not Found");
client.println("Content-Type: text/html");
client.println();
client.println("<h2>404</h2>");
delayTime = 250;
client.println("<h2>File Not Found!</h2>");
client.println("<br><br>");
client.println("<a href=http://" + publicIP + ":" +  LISTEN_PORT + "/SdBrowse    >Return to File Browser</a><br>");

Serial.println("Http 404 issued");

error = 1;

end();

} else {

client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/plain");
client.println("Content-Disposition: attachment");
client.println("Content-Length:");
client.println("Connnection: close");
client.println();

readFile();

end();

}

}

William

Pablo2048 commented 5 years ago

Sorry, but this is a mess, taken from middle of someone's code which probably does something different or is based on different prerequsities. My guess is no - this will not work.

Tech500 commented 5 years ago

I learn best by example. Anyone have an example of how to use a URL to download a "text/plain" file from SPIFFS? I lack the experience with Asyncwebserver to write this code.

I have been unable to locate this example searching the Internet.

William

Pablo2048 commented 5 years ago

William, you can not expect that you find an example that perfectly fits your needs. So let me explain how did i get the advices, which I wrote earlier: You have bunch of links to the .txt files in your HTML page, which are named from timestamp so we can not write simple handlers without heavy modificatin of your HTML generator. The simplest way to react to this is write your own onNotFound() handler. As you write, you don't know how to write onNotFound so i suggest you to take a look into examples, specially this https://github.com/me-no-dev/ESPAsyncWebServer/blob/f13685ee97675be2ac9502d177d3024ebc49c1e0/examples/simple_server/simple_server.ino#L25 - there is your simple onNotFound handler example and also how to integrate it into the server. This will be triggered on every not found url so we have to handle .txt files differently. How to do is simple - just detect, if url ends with .txt (it's simplified, but for your application is this enough IMO). So we have to modify the original code like this:

void notFound(AsyncWebServerRequest *request) {

  if (request->url().endsWith(F(".txt"))) {
    // here comes some mambo-jambo to extract the filename from request->url()

    // ... and finally
    request->send(SPIFFS, <filename>, String(), true);
  } else {
    request->send_P(404, PSTR("text/plain"), PSTR("Not found"));
  }
}

Now I challenge you to write some code to extract the filename from request->url(). If you take a look at the url string, produced from that method, you can see that the filename is at the end and starts with / so we can easily extract it from the url just by two lines of code:

    int fnsstart = request->url().lastIndexOf('/');
    String fn = request->url().substring(fnsstart);

At the end we should have in string variable fn filename like /LOG19102019.TXT . According to the documentation from here https://github.com/me-no-dev/ESPAsyncWebServer#respond-with-content-coming-from-a-file section Download index.htm we have technique on how to force downloading of ANY file we want - it actually does near the same thing as manually adding the "attachment" header - see here https://github.com/me-no-dev/ESPAsyncWebServer/blob/f13685ee97675be2ac9502d177d3024ebc49c1e0/src/WebResponses.cpp#L533 so if we put all this together we end with this:

void notFound(AsyncWebServerRequest *request) {

  if (request->url().endsWith(F(".txt"))) {
    // here comes some mambo-jambo to extract the filename from request->url()
    int fnsstart = request->url().lastIndexOf('/');
    String fn = request->url().substring(fnsstart);
    // ... and finally
    request->send(SPIFFS, fn, String(), true);
  } else {
    request->send_P(404, PSTR("text/plain"), PSTR("Not found"));
  }
}

Last thing is to remove serveStatic() from your code because we don't need it and add

serverAsync.onNotFound(notFound);

to your setup() and debug all this, because I write it only from my memory and reading the example, documentation and header from asyncwebserver. Pavel

Tech500 commented 5 years ago

Pavel,

Excellent memory, one small change for my code; instead of" .txt" needed ".TXT".

Thank you for your work helping me and your patience with me!

Project web site: Project web site

Attached code is still in the process of being finished; it is working for downloads of text files, thanks to Pavel. Hope it can help others in the future.

Issue Solved.

Documentation of project on "Hackster.io"

Respectfully. William

Pablo2048 commented 5 years ago

Glad to hear. You can probably close this issue as solved...