Browse Source

Add file explorer with browsing, upload, delete, create, rename and play audio features

master
grch101 5 years ago
committed by Torsten Stauder
parent
commit
8e678d79ef
  1. 1137
      html/index.html
  2. 410
      html/management.html
  3. 241
      src/main.cpp

1137
html/index.html
File diff suppressed because it is too large
View File

410
html/management.html

@ -117,6 +117,7 @@
<nav> <nav>
<div class="container nav nav-tabs" id="nav-tab" role="tablist"> <div class="container nav nav-tabs" id="nav-tab" role="tablist">
<a class="nav-item nav-link active" id="nav-rfid-tab" data-toggle="tab" href="#nav-rfid" role="tab" aria-controls="nav-rfid" aria-selected="true"><i class="fas fa-dot-circle"></i> RFID</a> <a class="nav-item nav-link active" id="nav-rfid-tab" data-toggle="tab" href="#nav-rfid" role="tab" aria-controls="nav-rfid" aria-selected="true"><i class="fas fa-dot-circle"></i> RFID</a>
<a class="nav-item nav-link" id="nav-files-tab" data-toggle="tab" href="#nav-files" role="tab" aria-controls="nav-files" aria-selected="false"><i class="fas fa-folder"></i> Dateien</a>
<a class="nav-item nav-link" id="nav-wifi-tab" data-toggle="tab" href="#nav-wifi" role="tab" aria-controls="nav-wifi" aria-selected="false"><i class="fas fa-wifi"></i><span class=".d-sm-none .d-md-block"> WLAN</span></a> <a class="nav-item nav-link" id="nav-wifi-tab" data-toggle="tab" href="#nav-wifi" role="tab" aria-controls="nav-wifi" aria-selected="false"><i class="fas fa-wifi"></i><span class=".d-sm-none .d-md-block"> WLAN</span></a>
%SHOW_MQTT_TAB% %SHOW_MQTT_TAB%
%SHOW_FTP_TAB% %SHOW_FTP_TAB%
@ -126,6 +127,21 @@
</nav> </nav>
<br> <br>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade" id="nav-files" role="tabpanel" aria-labelledby="nav-files-tab">
<div class="container" id="fileExplorer">
<div class="ui-widget">
<div class="filetree demo" id="explorerTree"></div>
<form id="explorerUploadForm" method="POST" enctype="multipart/form-data" action="/explorer" accept-charset="iso-8859-1">
<input id="explorerUploadedFiles" type="file" class="form-control-file" name="explorerUploadFiles" multiple> <input type="submit" class="btn btn-primary" id="submit" value="Hochladen">
</form>
<div>
<progress id="explorerUploadProgress" style="margin-top:10px" value="0" max="100"></progress> <span id="explorerUploadPercent"></span>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="nav-wifi" role="tabpanel" aria-labelledby="nav-wifi-tab"> <div class="tab-pane fade" id="nav-wifi" role="tabpanel" aria-labelledby="nav-wifi-tab">
<div class="container" id="wifiConfig"> <div class="container" id="wifiConfig">
<form action="#wifiConfig" method="POST" onsubmit="wifiConfig('wifiConfig'); return false"> <form action="#wifiConfig" method="POST" onsubmit="wifiConfig('wifiConfig'); return false">
@ -443,6 +459,399 @@
}); });
} }
// File Explorer functions begin
var lastSelectedNodePath = "";
$('#explorerTree').on('select_node.jstree', function (e, data) {
$('input[name=fileOrUrl]').val(data.node.data.path);
if (data.node.type == "folder") {
$('.option-folder').show();
$('.option-file').hide();
$('#playMode option').removeAttr('selected').filter('[value=3]').attr('selected', true);
}
if (data.node.type == "audio") {
$('.option-file').show();
$('.option-folder').hide();
$('#playMode option').removeAttr('selected').filter('[value=1]').attr('selected', true);
}
if(lastSelectedNodePath != data.node.data.path) {
if (data.node.data.directory) {
var ref = $('#explorerTree').jstree(true),
sel = ref.get_selected();
if(!sel.length) { return false; }
sel = sel[0];
var children = $("#explorerTree").jstree("get_children_dom",sel);
// refresh only, when there is no child -> possible not yet updated
if(children.length < 1){
refreshNode(sel);
}
}
lastSelectedNodePath = data.node.data.path;
}
});
function doRest(path, callback, obj) {
obj.url = path;
obj.dataType = "json";
obj.contentType= "application/json;charset=iso-8859-1",
obj.scriptCharset= "iso-8859-1",
obj.success = function(data, textStatus, jqXHR) {
if (callback) {
callback(data);
}
};
obj.error = function(jqXHR, textStatus, errorThrown) {
console.log("AJAX error");
//debugger;
};
jQuery.ajax(obj);
} // doRest
function getData(path, callback) {
doRest(path, callback, {
method : "GET"
});
} // getData
function deleteData(path, callback, _data) {
doRest(path, callback, {
method : "DELETE",
data: _data
});
} // deleteData
function patchData(path, callback, _data) {
doRest(path, callback, {
method : "PATCH",
data: _data
});
} // patchData
function postData(path, callback, _data) {
doRest(path, callback, {
method : "POST",
data: _data
});
} // postData
function putData(path, callback, _data) {
doRest(path, callback, {
method : "PUT",
data: _data
});
} // putData
// File Upload
$('#explorerUploadForm').submit(function(e){
e.preventDefault();
console.log("Upload!");
var data = new FormData(this);
var ref = $('#explorerTree').jstree(true),
sel = ref.get_selected(),
path = "/";
if(!sel.length) { alert("Please select the upload location!");return false; }
sel = sel[0];
selectedNode = ref.get_node(sel);
if(selectedNode.data.directory){
path = selectedNode.data.path
} else {
// remap sel to parent folder
sel = ref.get_node(ref.get_parent(sel));
path = parentNode.data.path;
console.log("Parent path: " + path);
}
$.ajax({
url: '/explorer?path=' + path,
type: 'POST',
data: data,
contentType: false,
processData:false,
xhr: function() {
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", function(evt) {
if (evt.lengthComputable) {
var percentComplete = evt.loaded / evt.total;
percentComplete = parseInt(percentComplete * 100);
console.log(percentComplete);
document.getElementById("explorerUploadProgress").value = percentComplete;
document.getElementById("explorerUploadPercent").innerHTML = percentComplete + "%";
if (percentComplete === 100) {
}
Sleep(1000);
}
}, false);
return xhr;
},
success: function(data, textStatus, jqXHR) {
console.log("Upload success!");
getData("/explorer?path=" + path, function(data) {
// We now have data!
deleteChildrenNodes(sel);
addFileDirectory(sel, data);
ref.open_node(sel);
});
}
});
});
// File Delete
function handleDeleteData(nodeId) {
var ref = $('#explorerTree').jstree(true);
var node = ref.get_node(nodeId);
var children = $("#explorerTree").jstree("get_children_dom",nodeId);
console.log(children.length);
if(node.data.directory) {
if(children.length > 0) {
for(var i=0;i<children.length;i++)
{
console.log("call delete function for: " + children[i].text);
handleDeleteData(children[i].id);
}
}
}
console.log("call delete request: " + node.data.path);
deleteData("/explorer?path=" + node.data.path);
}
function Sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
function fileNameSort( a, b ) {
if ( a.dir && !b.dir ) {
return -1
}
if ( !a.dir && b.dir ) {
return 1
}
if ( a.name < b.name ){
return -1;
}
if ( a.name > b.name ){
return 1;
}
return 0;
}
function createChild(nodeId, data) {
var ref = $('#explorerTree').jstree(true);
var node = ref.get_node(nodeId);
var child = {
text: data.name,
type: getType(data),
data: {
path: node.data.path + "/" + data.name,
directory: data.dir
}
}
return child;
}
function deleteChildrenNodes(nodeId) {
var ref = $('#explorerTree').jstree(true);
var children = $("#explorerTree").jstree("get_children_dom",nodeId);
for(var i=0;i<children.length;i++)
{
ref.delete_node(children[i].id);
}
}
function refreshNode(nodeId) {
var ref = $('#explorerTree').jstree(true);
var node = ref.get_node(nodeId);
getData("/explorer?path=" + node.data.path, function(data) {
// We now have data!
deleteChildrenNodes(nodeId);
addFileDirectory(nodeId, data);
ref.open_node(nodeId);
});
}
function getType(data) {
var type = "";
if(data.dir) {
type = "folder";
} else if ((/\.(mp3|MP3|ogg|wav|WAV|OGG|wma|WMA|acc|ACC|flac|FLAC)$/i).test(data.name)) {
type = "audio";
} else {
type = "file";
}
return type;
}
function addFileDirectory(parent, data) {
data.sort( fileNameSort );
var ref = $('#explorerTree').jstree(true);
for (var i=0; i<data.length; i++) {
console.log("Create Node");
ref.create_node(parent, createChild(parent, data[i]));
}
} // addFileDirectory
function buildFileSystemTree(path) {
$('#explorerTree').jstree({
"core" : {
"check_callback" : true,
'force_text' : true,
"themes" : { "stripes" : true },
'data' : { text: '/',
state: {
opened: true
},
type: 'folder',
children: [],
data: {
path: '/',
directory: true
}}
},
'types': {
'folder': {
'icon': "fa fa-folder"
},
'file': {
'icon': "fa fa-file"
},
'audio': {
'icon': "fa fa-file-audio"
},
'default': {
'icon': "fa fa-folder"
}
},
plugins: ["contextmenu", "themes", "types"],
contextmenu: {
items: function(nodeId) {
var ref = $('#explorerTree').jstree(true);
var node = ref.get_node(nodeId);
var items = {};
if (node.data.directory) {
items.createDir = {
label: "Neuer Ordner",
action: function(x) {
var childNode = ref.create_node(nodeId, {text: "Neuer Ordner", type: "folder"});
if(childNode) {
ref.edit(childNode, null, function(childNode, status){
putData("/explorer?path=" + node.data.path + "/" + childNode.text);
refreshNode(nodeId);
});
}
}
}
}
// Play
items.play = {
label: "Abspielen",
action: function(x) {
var playMode = node.data.directory?"5":"1";
postData("/exploreraudio?path=" + node.data.path + "&playmode=" + playMode);
}
}
// Refresh
items.refresh = {
label: "Aktualisieren",
action: function(x) {
refreshNode(nodeId);
}
}
// Delete
items.delete = {
label: "Loeschen",
action: function(x) {
handleDeleteData(nodeId);
refreshNode(ref.get_parent(nodeId));
}
}
// Rename
items.rename = {
label: "Umbenennen",
action: function(x) {
var srcPath = node.data.path;
ref.edit(nodeId, null, function(node, status){
node.data.path = node.data.path.substring(0,node.data.path.lastIndexOf("/")+1) + node.text;
patchData("/explorer?srcpath=" + srcPath + "&dstpath=" + node.data.path);
refreshNode(ref.get_parent(nodeId));
});
}
}
return items;
}
}
});
if (path.length == 0) {
return;
}
getData("/explorer?path=/", function(data) {
// We now have data!
$('#explorerTree').jstree(true).settings.core.data.children = [];
data.sort( fileNameSort );
for (var i=0; i<data.length; i++) {
var newChild = {
text: data[i].name,
type: getType(data[i]),
data: {
path: "/" + data[i].name,
directory: data[i].dir
},
children: []
}
$('#explorerTree').jstree(true).settings.core.data.children.push(newChild);
}
$("#explorerTree").jstree(true).refresh();
});
} // buildFileSystemTree
// File Explorer functions end
function renderFileTree() { function renderFileTree() {
var filesURI = "/files"; var filesURI = "/files";
@ -715,6 +1124,7 @@
$(document).ready(function () { $(document).ready(function () {
connect(); connect();
renderFileTree(); renderFileTree();
buildFileSystemTree("/");
console.log(parseInt(document.getElementById('warningLowVoltage').value)); console.log(parseInt(document.getElementById('warningLowVoltage').value));
$(function () { $(function () {

241
src/main.cpp

@ -358,6 +358,13 @@ void freeMultiCharArray(char **arr, const uint32_t cnt);
uint8_t getRepeatMode(void); uint8_t getRepeatMode(void);
bool getWifiEnableStatusFromNVS(void); bool getWifiEnableStatusFromNVS(void);
void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final); void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
void convertUtf8ToAscii(String utf8String, char *asciiString);
void explorerHandleFileUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final);
void explorerHandleListRequest(AsyncWebServerRequest *request);
void explorerHandleDeleteRequest(AsyncWebServerRequest *request);
void explorerHandleCreateRequest(AsyncWebServerRequest *request);
void explorerHandleRenameRequest(AsyncWebServerRequest *request);
void explorerHandleAudioRequest(AsyncWebServerRequest *request);
void headphoneVolumeManager(void); void headphoneVolumeManager(void);
bool isNumber(const char *str); bool isNumber(const char *str);
void loggerNl(const char *str, const uint8_t logLevel); void loggerNl(const char *str, const uint8_t logLevel);
@ -1446,8 +1453,11 @@ void playAudio(void *parameter) {
// If we're in audiobook-mode and apply a modification-card, we don't // If we're in audiobook-mode and apply a modification-card, we don't
// want to save lastPlayPosition for the mod-card but for the card that holds the playlist // want to save lastPlayPosition for the mod-card but for the card that holds the playlist
if(currentRfidTagId != NULL){
strncpy(playProperties.playRfidTag, currentRfidTagId, sizeof(playProperties.playRfidTag) / sizeof(playProperties.playRfidTag[0])); strncpy(playProperties.playRfidTag, currentRfidTagId, sizeof(playProperties.playRfidTag) / sizeof(playProperties.playRfidTag[0]));
} }
}
if (playProperties.trackFinished) { if (playProperties.trackFinished) {
playProperties.trackFinished = false; playProperties.trackFinished = false;
if (playProperties.playMode == NO_PLAYLIST) { if (playProperties.playMode == NO_PLAYLIST) {
@ -3586,7 +3596,7 @@ void webserverStart(void) {
wServer.addHandler(&events); wServer.addHandler(&events);
wServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { wServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send_P(200, "text/html", management_HTML, templateProcessor);
request->send(FSystem, "/index.html");
}); });
wServer.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request){ wServer.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request){
@ -3603,6 +3613,20 @@ void webserverStart(void) {
request->send(FSystem, DIRECTORY_INDEX_FILE, "application/json"); request->send(FSystem, DIRECTORY_INDEX_FILE, "application/json");
}); });
wServer.on("/explorer", HTTP_GET, explorerHandleListRequest);
wServer.on("/explorer", HTTP_POST, [](AsyncWebServerRequest *request) {
request->send(200);
}, explorerHandleFileUpload);
wServer.on("/explorer", HTTP_DELETE, explorerHandleDeleteRequest);
wServer.on("/explorer", HTTP_PUT, explorerHandleCreateRequest);
wServer.on("/explorer", HTTP_PATCH, explorerHandleRenameRequest);
wServer.on("/exploreraudio", HTTP_POST, explorerHandleAudioRequest);
wServer.onNotFound(notFound); wServer.onNotFound(notFound);
// allow cors for local debug // allow cors for local debug
@ -3683,6 +3707,221 @@ void webserverStart(void) {
return true; return true;
} }
// Conversion routine for http UTF-8 strings
void convertUtf8ToAscii(String utf8String, char *asciiString) {
uint16_t i = 0, j=0;
bool f_C3_seen = false;
while(utf8String[i] != 0) { // convert UTF8 to ASCII
if(utf8String[i] == 195){ // C3
i++;
f_C3_seen = true;
continue;
}
asciiString[j] = utf8String[i];
if(asciiString[j] > 128 && asciiString[j] < 189 && f_C3_seen == true) {
asciiString[j] = asciiString[j] + 64; // found a related ASCII sign
f_C3_seen = false;
}
i++; j++;
}
asciiString[j] = 0;
}
// Handles file upload request from the explorer
// requires a GET parameter path, as directory path to the file
void explorerHandleFileUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
if (!index) {
if (request->hasParam("path")) {
AsyncWebParameter *param = request->getParam("path");
String utf8FilePath = param->value() + "/" + filename;
char asciiFilePath[256];
convertUtf8ToAscii(utf8FilePath, asciiFilePath);
request->_tempFile = FSystem.open( asciiFilePath, "w");
} else {
request->_tempFile = FSystem.open("/" + filename, "w");
}
Serial.println("write file");
// open the file on first call and store the file handle in the request object
}
if (len) {
// stream the incoming chunk to the opened file
request->_tempFile.write(data, len);
}
if (final) {
// close the file handle as the upload is now done
request->_tempFile.close();
}
}
// Sends a list of the content of a directory as JSON file
// requires a GET parameter path for the directory
void explorerHandleListRequest(AsyncWebServerRequest *request) {
DynamicJsonDocument jsonBuffer(8192);
//StaticJsonDocument<4096> jsonBuffer;
String serializedJsonString;
AsyncWebParameter *param;
char asciiFilePath[256];
JsonArray obj = jsonBuffer.createNestedArray();
File root;
if(request->hasParam("path")){
param = request->getParam("path");
convertUtf8ToAscii(param->value(), asciiFilePath);
root = FSystem.open(asciiFilePath);
} else {
root = FSystem.open("/");
}
if (!root) {
snprintf(logBuf, serialLoglength, (char *) FPSTR(failedToOpenDirectory));
loggerNl(logBuf, LOGLEVEL_DEBUG);
return;
}
if (!root.isDirectory()) {
snprintf(logBuf, serialLoglength, (char *) FPSTR(notADirectory));
loggerNl(logBuf, LOGLEVEL_DEBUG);
return;
}
File file = root.openNextFile();
while(file) {
JsonObject entry = obj.createNestedObject();
std::string path = file.name();
std::string fileName = path.substr(path.find_last_of("/") + 1);
entry["name"] = fileName;
entry["dir"].set(file.isDirectory());
file = root.openNextFile();
}
serializeJson(obj, serializedJsonString);
request->send(200, "application/json; charset=iso-8859-1", serializedJsonString);
}
// Handles delete request of a file or directory
// requires a GET parameter path to the file or directory
void explorerHandleDeleteRequest(AsyncWebServerRequest *request) {
File file;
bool isDir;
AsyncWebParameter *param;
char asciiFilePath[256];
if(request->hasParam("path")){
param = request->getParam("path");
convertUtf8ToAscii(param->value(), asciiFilePath);
if(FSystem.exists(asciiFilePath)) {
file = FSystem.open(asciiFilePath);
isDir = file.isDirectory();
file.close();
if(isDir) {
if(FSystem.rmdir(asciiFilePath)) {
snprintf(logBuf, serialLoglength, "DELETE: %s deleted", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_INFO);
} else {
snprintf(logBuf, serialLoglength, "DELETE: Cannot delete %s", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_ERROR);
}
} else {
if(FSystem.remove(asciiFilePath)) {
snprintf(logBuf, serialLoglength, "DELETE: %s deleted", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_INFO);
} else {
snprintf(logBuf, serialLoglength, "DELETE: Cannot delete %s", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_ERROR);
}
}
} else {
snprintf(logBuf, serialLoglength, "DELETE: Path %s does not exitst", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_ERROR);
}
} else {
loggerNl("DELETE: No path variable set", LOGLEVEL_ERROR);
}
request->send(200);
}
// Handles create request of a directory
// requires a GET parameter path to the new directory
void explorerHandleCreateRequest(AsyncWebServerRequest *request) {
AsyncWebParameter *param;
char asciiFilePath[256];
if(request->hasParam("path")){
param = request->getParam("path");
convertUtf8ToAscii(param->value(), asciiFilePath);
if(FSystem.mkdir(asciiFilePath)) {
snprintf(logBuf, serialLoglength, "CREATE: %s created", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_INFO);
} else {
snprintf(logBuf, serialLoglength, "CREATE: Cannot create %s", asciiFilePath);
loggerNl(logBuf, LOGLEVEL_ERROR);
}
} else {
loggerNl("CREATE: No path variable set", LOGLEVEL_ERROR);
}
request->send(200);
}
// Handles rename request of a file or directory
// requires a GET parameter srcpath to the old file or directory name
// requires a GET parameter dstpath to the new file or directory name
void explorerHandleRenameRequest(AsyncWebServerRequest *request) {
AsyncWebParameter *srcPath;
AsyncWebParameter *dstPath;
char srcFullFilePath[256];
char dstFullFilePath[256];
if(request->hasParam("srcpath") && request->hasParam("dstpath")) {
srcPath = request->getParam("srcpath");
dstPath = request->getParam("dstpath");
convertUtf8ToAscii(srcPath->value(), srcFullFilePath);
convertUtf8ToAscii(dstPath->value(), dstFullFilePath);
if(FSystem.exists(srcFullFilePath)) {
if(FSystem.rename(srcFullFilePath, dstFullFilePath)) {
snprintf(logBuf, serialLoglength, "RENAME: %s renamed to %s", srcFullFilePath, dstFullFilePath);
loggerNl(logBuf, LOGLEVEL_INFO);
} else {
snprintf(logBuf, serialLoglength, "RENAME: Cannot rename %s", srcFullFilePath);
loggerNl(logBuf, LOGLEVEL_ERROR);
}
} else {
snprintf(logBuf, serialLoglength, "RENAME: Path %s does not exitst", srcFullFilePath);
loggerNl(logBuf, LOGLEVEL_ERROR);
}
} else {
loggerNl("RENAME: No path variable set", LOGLEVEL_ERROR);
}
request->send(200);
}
// Handles audio play requests
// requires a GET parameter path to the audio file or directory
// requires a GET parameter playmode
void explorerHandleAudioRequest(AsyncWebServerRequest *request) {
AsyncWebParameter *param;
String playModeString;
uint32_t playMode;
char asciiFilePath[256];
if(request->hasParam("path") && request->hasParam("playmode")) {
param = request->getParam("path");
convertUtf8ToAscii(param->value(), asciiFilePath);
param = request->getParam("playmode");
playModeString = param->value();
playMode = atoi(playModeString.c_str());
trackQueueDispatcher(asciiFilePath,0,playMode,0);
} else {
loggerNl("AUDIO: No path variable set", LOGLEVEL_ERROR);
}
request->send(200);
}
// Handles uploaded backup-file and writes valid entries into NVS // Handles uploaded backup-file and writes valid entries into NVS
void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { void handleUpload(AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {

Loading…
Cancel
Save