arendst / Tasmota

Alternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or rules, expandability and entirely local control over MQTT, HTTP, Serial or KNX. Full documentation at
https://tasmota.github.io/docs
GNU General Public License v3.0
22.03k stars 4.78k forks source link

Berry webserver: allow to content_send bytes #21479

Closed Staars closed 4 months ago

Staars commented 4 months ago

Description:

This small addition makes it possible to serve arbitrary data like images or any other file with binary data.

Typically prepended with a call to content_open to declare the MIME type:

    var data = bytes("0011223344")
    webserver.content_open(200,"application/octet-stream")
    webserver.content_send(data)

Longer demo in the comments.

Checklist:

NOTE: The code change must pass CI tests. Your PR cannot be merged unless tests pass

Staars commented 4 months ago

Very simple demo to download the files from the root folder "/" in a HTML page:

class DL
  var files

  def init()
    import path
    self.files = path.listdir("/")
    self.web_add_handler()
    tasmota.add_driver(self)
  end

  def web_add_button()
    import webserver
    webserver.content_send(
      "<form id='dl_demo' style='display: block;' action='Demo' method='get'><button>Download Demo</button></form><p></p>")
  end

  def download(args)
    import webserver
    var fpath = f"/{args}"
    var f = open(fpath,"r")
    var data = f.readbytes()
    webserver.content_open(200,"application/octet-stream")
    webserver.content_send(data)
    f.close()
  end

  #######################################################################
  # Display the complete page
  #######################################################################

  def getFileList()
    var el = "<div id='files'>"
    for file:self.files
      el += f"<a href='/b_dl?dl={file}' download='{file}'>{file}</a><br><br>"
    end
    el += "</div>"
    return el
  end

  def page_download()
    import webserver
    import json

    if !webserver.check_privileged_access() return nil end

    # regular web page
    webserver.content_start("Downloads")       #- title of the web page -#
    webserver.content_send_style()            #- send standard Tasmota styles -#

    webserver.content_send("<div>Berry Demo")
    webserver.content_send("<p>Download files from '/'</p></div><br>")        #- close .parent div-#
    webserver.content_send(self.getFileList())

    webserver.content_button(webserver.BUTTON_MANAGEMENT) #- button back to management page -#
    webserver.content_stop()                        #- end of web page -#
  end

  #######################################################################
  # Web Controller, called by POST to `/b_dl`
  #######################################################################
  def page_download_ctl()
    import webserver
    if !webserver.check_privileged_access() return nil end

    try
      if webserver.has_arg("dl")
        self.download(webserver.arg(0))
      else
        raise "value_error", "Unknown command"
      end
    except .. as e, m
      print(format("BRY: Exception> '%s' - %s", e, m))
      #- display error page -#
      webserver.content_start("Parameter error")      #- title of the web page -#
      webserver.content_send_style()                  #- send standard Tasmota styles -#

      webserver.content_send(format("<p style='width:340px;'><b>Exception:</b><br>'%s'<br>%s</p>", e, m))

      webserver.content_button(webserver.BUTTON_MANAGEMENT) #- button back to management page -#
      webserver.content_send("<p></p>")
      webserver.content_stop()                        #- end of web page -#
    end
  end

  def web_add_handler()
    import webserver
    #- we need to register a closure, not just a function, that captures the current instance -#
    webserver.on("/Demo", / -> self.page_download(), webserver.HTTP_GET)
    webserver.on("/b_dl", / -> self.page_download_ctl(), webserver.HTTP_GET)
  end
end

dl = DL()
sfromis commented 4 months ago

Nice that it seems to be possible now, since https://github.com/arendst/Tasmota/pull/15573

s-hadinger commented 4 months ago

I read back my comment from this PR. We need to check that the underlying implementation does not convert to String anymore

sfromis commented 4 months ago

Part of the context is also the not-really-documented #define UFILESYS_STATIC_SERVING supporting the UFSServe command to set up a web server from the file system, implying support for binary files. Not tested that....

Of course, that's not really equivalent to having Berry code able to directly serve binary data, which in some cases can be quite handy instead of needing to write such data to the file system first.

Anyway, if the webserver module can now handle binary data without going via the file system, "for completeness" it would be nice if the webclient module also could do it the other way, instead of needing to go via write_file for binary data.