diff --git a/_init.lua b/_init.lua
new file mode 100644
index 0000000..b6f6bc5
--- /dev/null
+++ b/_init.lua
@@ -0,0 +1,91 @@
+
+--
+-- File: _init.lua
+--[[
+ This is a template for the LFS equivalent of the SPIFFS init.lua.
+ It is a good idea to such an _init.lua module to your LFS and do most of the LFS
+ module related initialisaion in this. This example uses standard Lua features to
+ simplify the LFS API.
+ The first section adds a 'LFS' table to _G and uses the __index metamethod to
+ resolve functions in the LFS, so you can execute the main function of module
+ 'fred' by executing LFS.fred(params), etc. It also implements some standard
+ readonly properties:
+ LFS._time The Unix Timestamp when the luac.cross was executed. This can be
+ used as a version identifier.
+ LFS._config This returns a table of useful configuration parameters, hence
+ print (("0x%6x"):format(LFS._config.lfs_base))
+ gives you the parameter to use in the luac.cross -a option.
+ LFS._list This returns a table of the LFS modules, hence
+ print(table.concat(LFS._list,'\n'))
+ gives you a single column listing of all modules in the LFS.
+---------------------------------------------------------------------------------]]
+
+local index = node.flashindex
+
+local lfs_t = {
+ __index = function(_, name)
+ local fn_ut, ba, ma, size, modules = index(name)
+ if not ba then
+ return fn_ut
+ elseif name == '_time' then
+ return fn_ut
+ elseif name == '_config' then
+ local fs_ma, fs_size = file.fscfg()
+ return {lfs_base = ba, lfs_mapped = ma, lfs_size = size,
+ fs_mapped = fs_ma, fs_size = fs_size}
+ elseif name == '_list' then
+ return modules
+ else
+ return nil
+ end
+ end,
+
+ __newindex = function(_, name, value) -- luacheck: no unused
+ error("LFS is readonly. Invalid write to LFS." .. name, 2)
+ end,
+
+ }
+
+local G=getfenv()
+G.LFS = setmetatable(lfs_t,lfs_t)
+
+--[[-------------------------------------------------------------------------------
+ The second section adds the LFS to the require searchlist, so that you can
+ require a Lua module 'jean' in the LFS by simply doing require "jean". However
+ note that this is at the search entry following the FS searcher, so if you also
+ have jean.lc or jean.lua in SPIFFS, then this SPIFFS version will get loaded into
+ RAM instead of using. (Useful, for development).
+ See docs/en/lfs.md and the 'loaders' array in app/lua/loadlib.c for more details.
+---------------------------------------------------------------------------------]]
+
+package.loaders[3] = function(module) -- loader_flash
+ local fn, ba = index(module)
+ return ba and "Module not in LFS" or fn
+end
+
+--[[-------------------------------------------------------------------------------
+ You can add any other initialisation here, for example a couple of the globals
+ are never used, so setting them to nil saves a couple of global entries
+---------------------------------------------------------------------------------]]
+
+G.module = nil -- disable Lua 5.0 style modules to save RAM
+package.seeall = nil
+
+--[[-------------------------------------------------------------------------------
+ These replaces the builtins loadfile & dofile with ones which preferentially
+ loads the corresponding module from LFS if present. Flipping the search order
+ is an exercise left to the reader.-
+---------------------------------------------------------------------------------]]
+
+local lf, df = loadfile, dofile
+G.loadfile = function(n)
+ local mod, ext = n:match("(.*)%.(l[uc]a?)");
+ local fn, ba = index(mod)
+ if ba or (ext ~= 'lc' and ext ~= 'lua') then return lf(n) else return fn end
+end
+
+G.dofile = function(n)
+ local mod, ext = n:match("(.*)%.(l[uc]a?)");
+ local fn, ba = index(mod)
+ if ba or (ext ~= 'lc' and ext ~= 'lua') then return df(n) else return fn() end
+end
diff --git a/aps.lua b/aps.lua
new file mode 100644
index 0000000..9479f21
--- /dev/null
+++ b/aps.lua
@@ -0,0 +1,13 @@
+aplist = {}
+
+wifi.sta.getap(
+ function(t)
+ local k, v
+ local i = 0
+ for k,v in pairs(t) do
+ cprint("cfgsvr", k, v)
+ aplist[i] = k
+ i = i + 1
+ end
+ end
+ )
\ No newline at end of file
diff --git a/cfg.lua b/cfg.lua
new file mode 100644
index 0000000..e19669f
--- /dev/null
+++ b/cfg.lua
@@ -0,0 +1,10 @@
+cfg = {}
+--cfg.Mode = "AP"
+cfg.Mode = "Station"
+cfg.APServerSSID = "espfilemgr"
+cfg.APServerChannel = 6
+cfg.APServerPwd = nil
+cfg.APServerIP = "192.168.4.1"
+cfg.StationWiFiSSID = "YourWiFiSSID"
+cfg.StationWiFiPwd = "password"
+cfg.DebugLevel = 1
\ No newline at end of file
diff --git a/error404.html b/error404.html
new file mode 100644
index 0000000..17bce84
--- /dev/null
+++ b/error404.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+ File not found (name is case sensitive)
+
+
\ No newline at end of file
diff --git a/favicon.ico b/favicon.ico
new file mode 100644
index 0000000..b326df9
Binary files /dev/null and b/favicon.ico differ
diff --git a/files.html b/files.html
new file mode 100644
index 0000000..300466f
--- /dev/null
+++ b/files.html
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+
+
+ ESP IP or host:
+ Help
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Files to send:
+
+
+ If file exists:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Progress:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Delete the selected file?
+ Click the Delete button again to confirm
+
+
+
+
+
+ New file name
+ (Enter new name and click the Rename button again.)
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/files.js b/files.js
new file mode 100644
index 0000000..88dcacd
--- /dev/null
+++ b/files.js
@@ -0,0 +1,311 @@
+// This file depends rather heavily on elements within the html file
+// that contains it. These dependencies are resolved when script in
+// the container calls fileJsInit().
+var CHUNKSIZE = 511
+var xhr;
+var xhr2;
+var files;
+var file;
+var reader;
+var currentFileNdx = -1;
+var currentPos = 0;
+var currentChunk = 0;
+var buf = [];
+var urlPrefix;
+var option;
+var page;
+var isESP;
+
+// option possibilities are:
+// Overwrite : overwrites file automatically
+// Ignore : ignores existing file, setting-up failure
+// Abort : aborts transfer, fails with message "exists"
+// Backup : renames existing file by renaming with prefix "bu_(incremental_number)_"
+
+// Called by containing page when it loads to initialize element dependencies.
+function fileJsInit(obj) {
+ page = obj;
+}
+
+function sendFiles(fileInputObj) {
+ files = fileInputObj.files;
+ currentFileNdx = -1;
+ xhr = new XMLHttpRequest();
+ option = page.optionsListElement.value;
+ urlPrefix = location.protocol + "//" + page.serverElement.value;
+ sendNextFile();
+}
+
+function sendNextFile() {
+ currentFileNdx++;
+ file = files[currentFileNdx];
+ if (file == null)
+ return false;
+ currentChunk = 0;
+ buf = [];
+ reader = file.stream().getReader();
+ xhr.onreadystatechange = xhrReadyStateChange;
+ xhr.open("GET", urlPrefix + "/api/send/" + file.name + "/" + option, true);
+ xhr.send();
+}
+
+function xhrReadyStateChange() {
+ if (xhr.readyState == 4) {
+ var freeHeap = xhr.getResponseHeader("FreeHeap");
+ if (freeHeap)
+ page.heapElement.innerText = freeHeap;
+
+ if (xhr.status > 299) {
+ alert("HTTP status " + xhr.status.toString() + " " + xhr.statusText);
+ return;
+ }
+ if ((xhr.responseText.indexOf("success") < 0) && (xhr.status != 100)) {
+ alert(xhr.responseText);
+ return;
+ }
+
+ var respObj = JSON.parse(xhr.responseText);
+ isESP = (xhr.getResponseHeader("Server") == "ESPServer");
+
+ // buf is initialized to an empty array in sendNextFile()
+ // so it works the first time this function is called
+ if (currentPos >= buf.length) {
+ var promise = reader.read();
+ if (promise == undefined)
+ return;
+ promise.then(function (value) {
+ if ((value == undefined) || (value.value == undefined)) {
+ xhr.onreadystatechange = null;
+ xhr.open("GET", urlPrefix + "/api/persist/" + file.name + "/" + option, false);
+ xhr.send();
+ setTimeout(sendNextFile, 50);
+ getFileList();
+ return;
+ }
+ if (buf.length)
+ currentChunk++;
+ buf = value.value;
+ currentPos = 0;
+ })
+ }
+
+ if (respObj.bytes < file.size) {
+ var temp = buf.slice(currentPos, currentPos + CHUNKSIZE);
+ currentPos += temp.length;
+
+ xhr.open("POST", urlPrefix + "/api/append/" + file.name + "/" + option, true);
+ if (isESP) {
+ xhr.send(temp.buffer);
+ }
+ else {
+ var obj = createPostJson(file.name, temp);
+ xhr.setRequestHeader("Content-Type", "application/json");
+ xhr.send(JSON.stringify(obj));
+ }
+ }
+ if (page.statusElement) {
+ var bytesTransferred = (currentChunk * 65536) + currentPos;
+ var percentComplete = parseInt((bytesTransferred / file.size) * 100);
+ page.statusElement.value = "File: " + (currentFileNdx + 1).toString() + " of " + files.length.toString() + " " +
+ bytesTransferred.toString() + " bytes sent";
+ page.progressElement.style.width = percentComplete.toString() + "%";
+ page.progressElement.innerText = percentComplete.toString() + "%";
+ }
+
+ }
+}
+
+function createPostJson(fileName, data) {
+ return {
+ fileName: fileName,
+ data: data
+ }
+}
+
+
+function sendCfg() {
+ var xhrCfg = new XMLHttpRequest();
+ var cfgBuf = page.configElement.value;
+ urlPrefix = location.protocol + "//" + page.serverElement.value;
+ xhrCfg.onreadystatechange = null;
+ xhrCfg.open("GET", urlPrefix + "/api/send/cfg.lua/Overwrite", false);
+ xhrCfg.send();
+ xhrCfg.open("POST", urlPrefix + "/api/append/cfg.lua/Overwrite", false);
+ xhrCfg.send(cfgBuf);
+ xhrCfg.open("GET", urlPrefix + "/api/persist/cfg.lua/Overwrite", false);
+ xhrCfg.send();
+ page.configDiv.style.display = "none";
+}
+
+function getCfg() {
+ page.configDiv.style.display = "block";
+ var xhrCfg = new XMLHttpRequest();
+ urlPrefix = location.protocol + "//" + page.serverElement.value;
+ xhrCfg.onreadystatechange = null;
+ xhrCfg.open("GET", urlPrefix + "/cfg.lua", false);
+ xhrCfg.send();
+ page.configElement.value = xhrCfg.responseText;
+}
+
+
+function getFileList() {
+ urlPrefix = location.protocol + "//" + page.serverElement.value;
+ xhr2 = new XMLHttpRequest();
+ xhr2.onreadystatechange = xhrReadyStateChange2;
+ xhr2.open("GET", urlPrefix + "/api/list", true);
+ xhr2.send();
+}
+
+function restartESP() {
+ urlPrefix = location.protocol + "//" + page.serverElement.value;
+ xhr2 = new XMLHttpRequest();
+ xhr2.open("GET", urlPrefix + "/api/restart", false);
+ xhr2.send();
+}
+
+function getheap() {
+ if (page) {
+ urlPrefix = location.protocol + "//" + page.serverElement.value;
+ xhr2 = new XMLHttpRequest();
+ xhr2.onreadystatechange = null;
+ xhr2.open("GET", urlPrefix + "/api/heap", false);
+ xhr2.send();
+ page.heapElement.innerText = xhr2.responseText;
+ }
+}
+
+function updateHeap() {
+ window.setTimeout(updateHeap, 60000);
+ getheap();
+}
+
+//updateHeap();
+
+function xhrReadyStateChange2() {
+ if (xhr2.readyState == 4) {
+ var freeHeap = xhr2.getResponseHeader("FreeHeap");
+ if (freeHeap)
+ page.heapElement.innerText = freeHeap;
+
+ var list = JSON.parse(xhr2.responseText);
+ for (i = page.fileListElement.childNodes.length - 1; i >= 0; i--) {
+ page.fileListElement.removeChild(page.fileListElement.childNodes[i]);
+ }
+ var tr = document.createElement("tr");
+ tr = page.fileListElement.appendChild(tr);
+ var th = document.createElement("th");
+ th = tr.appendChild(th);
+ th.innerText = "File Name";
+ th = document.createElement("th");
+ th = tr.appendChild(th);
+ th.innerText = "Size";
+
+ // enumerate returned objects
+ for (i = 0; i < list.length; i++) {
+ var item = list[i];
+
+ tr = document.createElement("tr");
+ tr = page.fileListElement.appendChild(tr);
+ var td = document.createElement("td");
+ td = tr.appendChild(td);
+ var a = document.createElement("a");
+ a.href = item.name;
+ a.target = "_blank";
+ a.innerText = item.name
+ td.appendChild(a);
+ td = document.createElement("td");
+ td.align = "right";
+ td = tr.appendChild(td);
+ td.innerText = item.size;
+ }
+ populateFilesDropdown(list);
+ }
+}
+
+function populateFilesDropdown(list) {
+ for (i = page.fileDropdownElement.childNodes.length - 1; i >= 0; i--) {
+ page.fileDropdownElement.removeChild(page.fileDropdownElement.childNodes[i]);
+ }
+
+ // enumerate returned objects
+ for (i = 0; i < list.length; i++) {
+ var item = list[i];
+ var optionEl = document.createElement("option");
+ optionEl.value = item.name
+ optionEl.innerText = item.name
+ optionEl = page.fileDropdownElement.appendChild(optionEl);
+ }
+}
+
+function renameSelectedFile(that) {
+ urlPrefix = location.protocol + "//" + page.serverElement.value;
+ option = page.optionsListElement.value;
+ var oldName = page.fileDropdownElement.value;
+ if (page.newNameDivElement.style.display == "none") {
+ page.newNameDivElement.style.display = "block";
+ page.cancelFileOpBtnElement.style.display = "inline";
+ page.deleteFileBtnElement.style.display = "none";
+ that.innerText = "Confirm Rename";
+ return;
+ }
+ else {
+ that.innerText = "Rename Selected File";
+ page.cancelFileOpBtnElement.style.display = "none";
+ page.deleteFileBtnElement.style.display = "inline";
+ }
+ var newName = page.newFileNameInputElement.value;
+ if (newName == "") {
+ page.newNameDivElement.style.display = "none";
+ return;
+ }
+ xhr3 = new XMLHttpRequest();
+ xhr3.open("GET", urlPrefix + "/api/rename/" + oldName + "/" + newName + "/" + option, false);
+ xhr3.send();
+ page.newNameDivElement.style.display = "none";
+ page.newFileNameInputElement.value = "";
+ getFileList();
+}
+
+function deleteSelectedFile(that) {
+ option = page.optionsListElement.value;
+ urlPrefix = location.protocol + "//" + page.serverElement.value;
+ var name = page.fileDropdownElement.value;
+
+ if (page.deleteConfirmDivElement.style.display == "none") {
+ page.deleteConfirmDivElement.style.display = "block";
+ page.cancelFileOpBtnElement.style.display = "inline";
+ page.renameFileBtnElement.style.display = "none";
+ that.innerText = "Confirm Delete";
+ return;
+ }
+ else {
+ that.innerText = "Delete Selected File";
+ page.cancelFileOpBtnElement.style.display = "none";
+ page.renameFileBtnElement.style.display = "inline";
+ }
+
+ var appFilesList = ["error404.html", "files.js", "server.lua", "wificfgsvr.lua", "files.html", "help.html", "favicon.ico", "_init.lua", "index.html", "cfg.lua", "init.lua"];
+ if (appFilesList.indexOf(name) >= 0) {
+ if (!confirm("The file " + name + " is part of the FileMgr application, deleting it may break the app.\r\n\r\nAre you sure?")) {
+ page.deleteConfirmDivElement.style.display = "none";
+ return;
+ }
+ }
+
+ xhr3 = new XMLHttpRequest();
+ xhr3.open("GET", urlPrefix + "/api/delete/" + name + "/" + option, false);
+ xhr3.send();
+ getFileList();
+ page.deleteConfirmDivElement.style.display = "none";
+}
+
+function cancelDeleteRename(that) {
+ page.cancelFileOpBtnElement.style.display = "none";
+ page.deleteFileBtnElement.innerText = "Delete Selected File";
+ page.deleteFileBtnElement.style.display = "inline";
+ page.renameFileBtnElement.innerText = "Rename Selected File";
+ page.renameFileBtnElement.style.display = "inline";
+ page.newNameDivElement.style.display = "none";
+ page.deleteConfirmDivElement.style.display = "none";
+}
+
diff --git a/fileupload.lua b/fileupload.lua
new file mode 100644
index 0000000..ca1cfe6
--- /dev/null
+++ b/fileupload.lua
@@ -0,0 +1,75 @@
+-- {version: "1.0.0"}
+local module =...
+ return function(conn, pname)
+ CHUNKSIZE = 256
+ MAXNAMELEN = 18
+ local buf
+ local fname = string.sub(pname, 1, MAXNAMELEN)
+ tmr.wdclr()
+ if file.exists(fname) then
+ s, e = string.find(string.reverse(fname), ".", 1, true)
+ local ext = string.lower(string.sub(fname, -(s - 1)))
+ cprint(ext, 1)
+ local contentType
+
+ if (ext == "html" or ext == "htm" or ext == "js" or ext == "json" or ext == "txt") then
+ contentType = contentTypeHtml
+ elseif (ext == "jpg" or ext == "png" or ext == "bmp" or ext == "ico" or ext == "gif" or ext == "jpeg") then
+ contentType = contentTypeImage
+ else
+ contentType = contentTypeBin
+ end
+
+ buf = "HTTP/1.1 200 OK" .. contentType .. headerBlock
+ else
+ fname = "error404.html"
+ buf = "HTTP/1.1 404 FILE NOT FOUND" .. contentTypeHtml .. headerBlock
+ end
+
+ local function unloadModule()
+ if module ~= nil then
+ package.loaded[module] = nil
+ cprint("unloading fileupload", 0)
+ end
+ module = nil
+ end
+
+ conn:on ("sent",
+ function(sck)
+ function sendfile(sck)
+ local f = getFileObject(sck, fname, "r")
+ buf = f:read(CHUNKSIZE)
+ if buf ~= nil then
+ cprint("sent " .. #buf .. " bytes, heap: " .. node.heap(), 4)
+ sck:send(buf)
+ else
+ closeFileObject(sck, fname)
+ cprint("file:read returned nil, closed file heap: " .. node.heap(), 2)
+ sck:close()
+ unloadModule()
+ return
+ end
+ end
+ sck:on("sent", sendfile)
+ sck:on("disconnection",
+ function(sck)
+ cprint("disconnection fileupload.sendfile, heap: " .. node.heap(), 0)
+ closeFileObject(sck, fname)
+ unloadModule()
+ return
+ end
+ )
+ sendfile(sck)
+ end
+ )
+ conn:on ("receive",
+ function(sck, pl)
+ cprint("received data closing connection, heap: " .. node.heap(), 0)
+ sck:close()
+ end
+ )
+ if buf == nil then
+ buf = ""
+ end
+ conn:send(buf)
+ end
\ No newline at end of file
diff --git a/help.html b/help.html
new file mode 100644
index 0000000..7fd799f
--- /dev/null
+++ b/help.html
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
ESP File Manager for NodeMCU
+
(Developed using NodeMCU 3.0-master, complete build details below)
+
+ This app allows files of any size or type to be uploaded to and downloaded from an ESP-8266,
+ using any browser [within reason.].
+
+
+
+ When uploading a file, if a file of that name already exists, behavior is dictated by the "if file exists..."
+ pulldown:
+
+
+
Backup: Renames the existing file to filename(1).ext. If that file exists it is deleted (only one backup copy is kept.)
+
Overwrite: The original file is overwritten when the upload completes.
+
Abort: If the file exists the ESP server returns status 403 and the upload is aborted.
+
+
+
Capabilities:
+
+
No file size limit (up to the capacity of the ESP.)
+
All file types are supported, text or binary.
+
HTML files and images are rendered in a new browser tab.
+
Multiple files can be selected for upload.
+
Long file names are supported.
+
Up to 5 silmultaneous file downloads (from ESP) are supported. (The sixth exceeds memory capacity.)
+
Up to 2 silmultaneous file uploads (to ESP) are supported. (Depending on other activity, anything more than one concurrent upload risks exceeding memory capacity.)
+
Provides a directory listing, with a download link for each file.
+
Allows files to be deleted and renamed.
+
Operates as either a stand-alone AP or as a station connected to your AP.
+
The current ESP free heap size is sent as a header with most responses, and can be explicitly requested as well.
+
The web page provides a way to edit the app's configuration file and update it on the ESP.
+
+
+
Limitations:
+
+
Multiple silmultaneous downloads using the same file name, from the same IP address, are not supported. (i.e., the remote host IP + filename must be unique.)
+
Multiple silmultaneous uploads to the same file name from any hosts, is unsupported.
+
File corruption will be the consequence if either of the two conditions above occur.
+
Error checking capabilities are extremely limited.
+
The upload user interface is the standard input type=file element, however, the upload protocol is not standard, the script code in files.js must be used.
+
+
+
Setup:
+
+
Edit the file cfg.lua to configure this app for your environment. (See details below.)
+
Copy all files in this folder to the ESP file system, using ESplorer or similar.
+
Reset the ESP.
+
+
+
Configuration:
+
+
The ESP can operate in either station or AP mode.
+
+ To configure, edit cfg.lua
+
+
+ AP Mode:
+
+
cfg.Mode: set the value to "AP". Only the values that start with "AP" apply to this mode.
+
The default values create an open AP using channel 6 with SSID "espfilemgr" using IP address 192.168.4.1
+
+
+
+ Station Mode:
+
+
cfg.Mode: set the value "Station". Only the values that start with "Station" apply to this mode.
+
cfg.StationWiFiSSID: Identifies the AP the ESP will connect to.
+
cfg.StationWiFiPwd: The password for the AP that the ESP will connect to.
+
+
+
+
+
+
+
+
Usage:
+
+
The ESP can operate in either station or AP mode.
+
+ Connect to the ESP via network
+
+
If in AP mode you must connect to the ESP as a client using another WiFi device.
+
If in station mode, the ESP connects to your WiFi AP and it can be accessed by any other device on the network.
+
+
+
+ Use the IP address of the ESP to browse to it using your web browser. Examples:
+
+
http://192.168.4.1/files.html (Default address in AP mode.)
+
http://192.168.x.x/files.html (In station mode you must determine the IP address that was assigned, details below.)
+
+
+
+
+
+
To determine which IP was assigned to the ESP by your network:
+
+
+ In station mode there are a few ways to determine the IP:
+
+
Monitor the ESP's UART output while it is booting, it will dump its assigned IP after connecting.
+
Find it in your WiFi AP configuration's list of connected devices (or DHCP clients.)
+
+ Probe the network for it using a PC:
+
+
+ Before powering on the ESP open a console window and execute the command: PING 192.168.255.255<enter> then
+ execute ARP -A<enter> Note the IP addresses listed in the output.
+
+
Power up the ESP and give it a minute or two to connect.
+
+ Re-execute the same console commands, look for a new address in the output, use that address to browse to it. e.g.,
+ if the new address in the list is 192.168.0.6, browse to http://192.168.0.6/files.html or
+ http://192.168.0.6/ (the default file is index.html, which contains a client-side
+ redirect to files.html)
+
+
+
+
+
+
In AP mode the IP of the ESP is set in the configuration file cfg.lua, default is 192.168.4.1
+
+
+
+ If you want to incorporate this functionality in another app, and you have already configured/connected to a network,
+ copy all of the code from init.lua except for the last two lines, to your init file. You will need to assign either the variable ip or serverip,
+ depending on mode. Then when you want this functionality to be available execute server.lua, e.g., dofile("server.lua") To disable the
+ functionality and recover the memory used by server.lua, reset the ESP.
+
+
+
LFS:
+
+
+ You can get an extra 10K or so of memory or your ESP by using LFS (a thorough discussion of which can be found
+ here.)
+
+
+ To enable LFS:
+
+
Upload lfs.img to the ESP
+
Execute the command node.flashreload("lfs.img") on the ESP
+
+
+
+ If you make any changes to the 3 files that are included in the LFS image, you must recompile them and update the ESP:
+
+
Add the files server.lua, wificfgsvr.lua and fileupload.lua to a zip file, name it lfs.zip.
+
Upload the lfs.zip file to this site to compile it into an LFS image.
+
The site will send lfs.img as a download.
+
Upload lfs.img to the ESP
+
Execute the command node.flashreload("lfs.img") on the ESP
+
Delete server.lua, wificfgsvr.lua and fileupload.lua from the ESP
+
+
+
+ een, doeseen, does
+
API Documentation
+
+
[file name] - name of file, URL encoded if necessary
+
+ [option]
+
+
Backup - If file exists, it is renamed to keep as a backup.
+
Overwrite - If file exists, it is overwritten.
+
Abort - If file exists, transfer is aborted.
+
+
+
/api/send/[file name]/[option]
+
/api/append/[file name] (POST)
+
/api/persist/[file name]/[option]
+
/api/rename/[old file name]/[new file name]
+
/api/delete/[file name]
+
/api/restart
+
/api/heap
+
/api/dofile/[file name]
+
/api/version/[file name]
+
+
+
Build details:
+
+NodeMCU 3.0.0.0 built on nodemcu-build.com provided by frightanic.com
+ branch: master
+ commit: 310faf7fcc9130a296f7f17021d48c6d717f5fb6
+ release: 3.0-master_20190907
+ release DTS: 201909070945
+ SSL: false
+ build type: integer
+ LFS: 0x0
+ modules: file,gpio,net,node,tmr,uart,wifi
+build 2019-12-04 04:30 powered by Lua 5.1.4 on SDK 3.0.1-dev(fce080e)
+
+
+
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..d3bb488
--- /dev/null
+++ b/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/init.lua b/init.lua
new file mode 100644
index 0000000..f609356
--- /dev/null
+++ b/init.lua
@@ -0,0 +1,27 @@
+--,init,lua,1,0,1,0
+
+dofile("_init.lua")
+
+dofile("cfg.lua")
+
+-- Last argument MUST be numeric, debug level
+function cprint(...)
+ local arglist = {...}
+ local level = arglist[#arglist]
+ if level <= cfg.DebugLevel then
+ print(...)
+ end
+end
+
+ip = nil
+serverip = nil
+initStage = 0
+failureCnt = 0
+versions = nil
+
+--aplist = {}
+cfg.APMac = wifi.ap.getmac()
+tmrSvrCfg = tmr.create()
+tmrSvrCfg:alarm(4000, tmr.ALARM_AUTO, require("wificfgsvr"))
+
+
diff --git a/lfs.img b/lfs.img
new file mode 100644
index 0000000..c967fd5
Binary files /dev/null and b/lfs.img differ
diff --git a/server.lua b/server.lua
new file mode 100644
index 0000000..eaef63d
--- /dev/null
+++ b/server.lua
@@ -0,0 +1,298 @@
+-- {"version": "1.0.0"}
+contentTypeHtml = "\r\nContent-type: text/html"
+contentTypeBin = "\r\nContent-type: application/octet-stream"
+contentTypeImage = "\r\nContent-type: image/jpg"
+headerBlock = "\r\nConnection: close\r\nServer: ESPServer\r\nAccess-Control-Allow-Origin: *\r\nCache-Control: no-cache\r\n\r\n"
+local currentFileName = ""
+local isPostData = false
+local retval = ""
+local success = "{ \"status\": \"success\", \"bytes\": "
+local option
+local isResetting = false
+print("filexfer server")
+activeClients = {}
+
+function getFreeHeapHeader()
+ return "\r\nFreeHeap: " .. node.heap()
+end
+
+function getFileObject(sck, fileName, mode)
+ local port, ip = sck:getpeer()
+ if (activeClients[ip] == nil) then
+ activeClients[ip] = {}
+ cprint("creating active client for " .. ip, 2)
+ end
+ if (activeClients[ip][fileName] == nil) then
+ activeClients[ip][fileName] = file.open(fileName, mode)
+ cprint("opening file handle " .. fileName, 3)
+ end
+ return activeClients[ip][fileName]
+end
+
+function closeFileObject(sck, fileName)
+ local port, ip = sck:getpeer()
+ if (activeClients[ip] ~= nil) then
+ if (activeClients[ip][fileName] ~= nil) then
+ activeClients[ip][fileName]:close()
+ activeClients[ip][fileName] = nil
+ cprint("closing file handle " .. fileName, 3)
+ end
+ end
+end
+
+local srv=net.createServer(net.TCP, 60)
+srv:listen(80,
+ function(conn)
+ assignHandlers(conn)
+ end
+)
+
+function assignHandlers(conn)
+ conn:on("disconnection", disconnection)
+ conn:on("sent", sent)
+ conn:on("receive", receive)
+end
+
+local function getfilesize(name)
+ local stat = file.stat(name)
+ local tempbuf = ""
+ if stat then
+ tempbuf = stat.size
+ cprint(name .. " " .. stat.size, 2)
+ end
+ return tempbuf
+end
+
+local function writefile(sck, name, mode, data)
+ local f = getFileObject(sck, "t_" .. name, mode)
+ if (f == nil) then
+ return -1
+ end
+ f:write(data)
+ closeFileObject(sck, "t_" .. name)
+ f = nil
+ return success.. getfilesize("t_" .. name).. "}"
+end
+
+local function processOption(name, opt, isComplete)
+ if (#name > 18) then
+ return false
+ end
+ if (file.exists(name) == false) then
+ retval = success .. "}"
+ return true
+ end
+
+ if (opt == "Backup") then
+ if (isComplete == true) then
+ s, e = string.find(string.reverse(name), ".", 1, true)
+ local ext = string.sub(name, -s)
+ local buName = string.sub(name, 1, -(s + 1)) .. "(1)" .. ext
+ cprint(buName, 2)
+
+ if (file.exists(buName)) then
+ file.remove(buName)
+ end
+ file.rename(name, buName)
+ end
+ retval = success .. getfilesize(name) .. "}"
+ return true
+ end
+ if (opt == "Overwrite") then
+ if (isComplete == true) then
+ file.remove(name)
+ end
+ retval = success .. "}"
+ return true
+ end
+ if (opt == "Ignore") then
+ retval = success .. "}"
+ return true
+ end
+ if (opt == "Abort") then
+ retval = "\"error file exists\"}"
+ return false
+ end
+end
+
+function disconnection(conn)
+ cprint("disconnection", 1)
+ isPostData = false
+end
+
+function sent(conn)
+ if (isPostData ~= true) then
+ currentFileName = ""
+ isPostData = false
+ conn:close()
+ cprint("onsent closing connection", 1)
+ end
+end
+
+function receive(conn, payload)
+ tmr.wdclr();
+ local s, e, m, buf, k, v
+ local tbl = {}
+ local i = 1
+ local method
+
+ s, e = string.find(payload, "HTTP", 1, true)
+ if (isPostData and (e == nil)) then
+ retval = writefile(conn, currentFileName, "a+", payload)
+ cprint("ispostdata raw data" .. #payload, 4)
+ payload = nil
+ --isPostData = false
+ cprint("sending status 100", 4)
+ buf = "HTTP/1.1 100 CONTINUE" .. contentTypeHtml .. getFreeHeapHeader() .. headerBlock
+ conn:send(buf)
+ return
+ else
+ if e ~= nil then
+ buf = string.sub(payload, 1, s - 2)
+ for m in string.gmatch(buf, "/?([%w+%p+][^/+]*)") do
+ tbl[i] = m
+ cprint(i .. " " .. m, 5)
+ i = i + 1
+ end
+ m = nil
+ method = tbl[1]
+ cprint(#tbl .. " " .. method, 1)
+
+ if tbl[2] == "api" then
+ local cmd = tbl[3]
+ if (tbl[4] ~= nil) and (tbl[4] ~= "/") then
+ currentFileName = tbl[4]
+ end
+ if (tbl[5] ~= nil) then
+ option = tbl[5]
+ end
+ -- option is always the last parameter,
+ -- for rename it will be at index 5
+ if (tbl[6] ~= nil) then
+ option = tbl[6]
+ end
+
+ cprint("cmd " .. cmd, 1)
+ if (cmd == "restart") then
+ retval = "apparent failure"
+ isResetting = true
+ node.restart()
+ return
+ end
+
+ if (cmd == "heap") then
+ retval = node.heap()
+ end
+
+ if (cmd == "dofile") then
+ retval = dofile(currentFileName)
+ end
+
+ if (cmd == "send") then
+ if (#currentFileName > 18) then
+ buf = "HTTP/1.1 409 INVALID FILE NAME" .. contentTypeHtml .. getFreeHeapHeader() .. headerBlock
+ conn:send(buf)
+ return
+ end
+ if (processOption(currentFileName, option, false)) then
+ retval = writefile(conn, currentFileName, "w+", "")
+ cprint(retval, 2)
+ else
+ buf = "HTTP/1.1 403 FILE EXISTS" .. contentTypeHtml .. getFreeHeapHeader() .. headerBlock
+ conn:send(buf)
+ return
+ end
+ end
+
+ if (cmd == "append") then
+ s, e = string.find(payload, "\r\n\r\n", 1, true)
+ cprint("payload length " .. #payload, 4)
+ isPostData = true
+ if e ~= nil then
+ buf = string.sub(payload, s + 4)
+ cprint("data length " .. #buf .. " " .. s .. ' ' .. e, 5)
+ if #buf > 0 then
+ retval = writefile(conn, currentFileName, "a+", buf)
+ cprint(retval, 3)
+ else
+ isPostData = false
+ end
+ --else
+ -- retval = writefile(conn, currentFileName, "a+", payload)
+ end
+ end
+
+ if (cmd == "persist") then
+ if (processOption(currentFileName, option, true)) then
+ file.rename("t_" .. currentFileName, currentFileName)
+ retval = success.. getfilesize(currentFileName).. "}"
+ end
+ end
+ if (cmd == "rename") then
+ if (processOption(tbl[5], option, true)) then
+ file.rename(currentFileName, tbl[5])
+ end
+ end
+
+ if (cmd == "delete") then
+ file.remove(currentFileName)
+ end
+
+ if (cmd == "list") then
+ -- get list of files and send to client
+ local listBuf = "[{\"name\":\".\",\"size\": 0}"
+ l = file.list();
+ for k,v in pairs(l) do
+ listBuf = listBuf..",{\"name\":\""..k.."\",\"size\":"..v.."}"
+ end
+ listBuf = listBuf.."]"
+
+ buf = "HTTP/1.1 200 OK" .. contentTypeHtml .. getFreeHeapHeader() .. headerBlock .. listBuf
+ conn:send(buf)
+ payload = nil
+ tbl = nil
+ l = nil
+ listBuf = nil
+ return
+ end
+
+ if (cmd == "version") then
+ local f = getFileObject(conn, currentFileName, "r")
+ f:seek("set", 2)
+ buf = "HTTP/1.1 200 OK" .. contentTypeHtml .. getFreeHeapHeader() .. headerBlock .. f:readline()
+ closeFileObject(conn, currentFileName)
+ f = nil
+ conn:send(buf)
+ payload = nil
+ tbl = nil
+ return
+ end
+
+ buf = ""
+ if retval == nil then
+ retval = "[nil]"
+ end
+ else
+ -- if no command was present the client wants to download an existing file.
+ -- default document name is hard-coded in the line below
+ local filename = "index.html"
+ if tbl[2] ~= nil and tbl[2] ~= "/" then
+ filename = tbl[2]
+ end
+ closeFileObject(conn, filename)
+ cprint("calling upload " .. filename, 1)
+ require("fileupload")(conn, filename)
+ buf = nil
+ payload = nil
+ tbl = nil
+ return
+ end
+ end
+ end
+ buf = "HTTP/1.1 200 OK" .. contentTypeHtml.. getFreeHeapHeader() .. headerBlock .. retval
+ payload = nil
+ tbl = nil
+ if isResetting == false then
+ conn:send(buf)
+ end
+end
\ No newline at end of file
diff --git a/wificfgsvr.lua b/wificfgsvr.lua
new file mode 100644
index 0000000..58952bd
--- /dev/null
+++ b/wificfgsvr.lua
@@ -0,0 +1,128 @@
+-- {"version": "1.0.0"}
+local module =...
+ return function()
+ cprint("[wificfgsvr.lua]", initStage, 1)
+ ----cprint(node.heap(), 2)
+ if (initStage == 0) then
+ --cprint("[wifi.setmode]", 3)
+ wifi.setmode(wifi.STATIONAP)
+ if (cfg.Mode == "AP") then
+ nextCfgStep = 5
+ else
+ nextCfgStep = 1
+ end
+ initStage = nextCfgStep
+ return
+ end
+ if (initStage == 1) then
+ if cfg.WiFiPwd == nil then
+ cfg.WiFiPwd = ""
+ end
+ local staconfig = {}
+ staconfig.ssid = cfg.StationWiFiSSID
+ staconfig.pwd = cfg.StationWiFiPwd
+ wifi.sta.config(staconfig)
+ initStage = 2
+ return
+ end
+ if (initStage == 2) then
+ --cprint("[wifi.connect]", 3)
+ wifi.sta.connect()
+ initStage = 3
+ return
+ end
+ if (initStage == 3) then
+ ip = wifi.sta.getip()
+ initStage = 4
+ return
+ end
+
+ if initStage == 4 then
+ if ip ~= nil then
+ initStage = 5
+ cprint(ip, 0)
+ tmrSvrCfg:unregister()
+ node.task.post(function()
+ dofile("server.lua")
+ end)
+ return
+ else
+ --cprint("[no sta ip]", 5)
+ initStage = 1
+ failureCnt = failureCnt + 1
+ if failureCnt > 10 then
+ node.restart()
+ end
+ return
+ end
+ end
+
+ if initStage == 5 then
+ --cprint("[AP config]", 5)
+ local wificfg={
+ ssid = cfg.APServerSSID,
+ auth = AUTH_OPEN,
+ channel = cfg.APServerChannel}
+
+ if cfg.APServerPwd ~= nil then
+ if #cfg.APServerPwd >= 10 then
+ wificfg.pwd = cfg.APServerPwd
+ end
+ end
+ wifi.ap.config(wificfg)
+ initStage = 6
+ wificfg = nil
+ return
+ end
+
+ if initStage == 6 then
+ --cprint("[server ip]", 5)
+ local ipcfg = {
+ ip=cfg.APServerIP,
+ netmask="255.255.255.0",
+ gateway=cfg.APServerIP}
+ wifi.ap.setip(ipcfg)
+ initStage = 7
+ return
+ end
+
+ if initStage == 7 then
+ serverip= wifi.ap.getip()
+ cprint("[server ip]: ",serverip, 1)
+ if serverip ~= nil then
+ tmrSvrCfg:unregister()
+ initStage = 0
+ cprint("[ready to connect]", node.heap(), 1)
+
+ failureCnt = 0
+ node.task.post(function()
+ dofile("server.lua")
+ end)
+ if module then
+ package.loaded[module] = nil
+ module = nil
+ end
+
+ return
+
+ -- wifi.sta.getap(
+ -- function(t)
+ -- local k, v
+ -- local i = 0
+ -- for k,v in pairs(t) do
+ -- cprint("cfgsvr", k, v, 5)
+ -- aplist[i] = k
+ -- i = i + 1
+ -- end
+ -- end
+ -- )
+ else
+ initStage = 5
+ failureCnt = failureCnt + 1
+ if failureCnt > 10 then
+ node.restart()
+ end
+ --cprint("no server ip", 5)
+ end
+ end
+ end