First the problem.
The user can upload file from the web with ajax. If the file is relatively big, the uploading takes a while. If the user's connection is lost or something happens during the uploading process, the file is going to be damaged or empty.
How should I secure the upload process so the file remains the same if it fails for some reason?
I'm using the following libraries on the Arduino ESP32:
I have a basic file upload handler on my esp32 which looks like this:
server.on("/uploading", HTTP_POST, [](AsyncWebServerRequest * request) {
}, handleFileUpload);
void handleFileUpload(AsyncWebServerRequest * request, String filename,size_t index, uint8_t *data, size_t len, bool final) {
if (!index) {
if (!filename.startsWith("/"))
filename = "/" + filename;
if (LITTLEFS.exists(filename)) {
LITTLEFS.remove(filename);
}
uploadFile = LITTLEFS.open(filename, "w");
}
for (size_t i = 0; i < len; i++) {
uploadFile.write(data[i]);
}
if (final) {
uploadFile.close();
if(filename == "/myHomeProgram.json"){initProgram = true;}
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", "File Uploaded;"+filename);
response->addHeader("Access-Control-Allow-Origin","*");
request->send(response);
}
}
This is working pretty well, the files are uploaded correctly 99% of the cases, but if it fails I lost the file data, or if some other part of the program wants to open the same file it fails too. Should I write to a temporary file and after if it succeeded write the content to the intended file somehow? Here is an example from client ( JS ) side:
// Example call:
saveFile(JSON.stringify(places),"/myHomeProgram.json","application/json");
function saveFile(data, filename, type) {
var file = new Blob([data], {type: type});
form = new FormData();
form.append("blob", file, filename);
$.ajax({
url: '/uploading',
type: 'POST',
data: form,
processData: false,
contentType: false
}).done(function(resp){
var response = resp.split(";");
$(".saveIconGraph").removeClass("fas fa-spinner fa-spin");
$(".saveIconGraph").addClass("far fa-save");
if(response[1] == "/myHomeProgram.json"){
toast("success","saveOk","progInfo",3500);
showSaved();
setTimeout(() => {
$("#saveMe").fadeOut( "slow", function() {
showSave();
});
}, 1000);
initPlaces();
}
}).fail(function(resp){
var response = resp.split(";");
$(".saveIconGraph").removeClass("fas fa-spinner fa-spin");
$(".saveIconGraph").addClass("far fa-save");
if(response[1] == "/myHomeProgram.json"){
toast("error","saveNotOk","progInfo",3500);
showSaveError();
$("#saveMeBtn").addClass("shakeEffect");
setTimeout(() => {
$("#saveMeBtn").removeClass("shakeEffect");
showSave();
}, 4500);
}
});
}
I could save the file in a temporary char variable before write, and on the final I could match the size of the file and the temporary variable size and if it is not the same, roll back to the previous. Is this manageable?
Something like this:
String uploadTemp = "";
inline boolean saveFileToTemp(String fileName){
uploadTemp = "";
File f = LITTLEFS.open(fileName, "r");
if (!f) {
f.close();
return false;
}else{
for (int i = 0; i < f.size(); i++){
uploadTemp += (char)f.read();
}
}
f.close();
return true;
}
inline boolean revertBackFile(String fileName){
File g = LITTLEFS.open(fileName, "w");
if (!g) {
g.close();
return false;
}else{
g.print(uploadTemp);
}
g.close();
return true;
}
inline boolean matchFileSizes(String fileName,boolean isFileExists){
boolean isCorrect = false;
if(isFileExists){
File writedFile = LITTLEFS.open(fileName, "w");
if( writedFile.size() == uploadTemp.length()){
isCorrect = true;
}else{
isCorrect = false;
}
writedFile.close();
return isCorrect;
}else{
return true;
}
}
void handleFileUpload(AsyncWebServerRequest * request, String filename,size_t index, uint8_t *data, size_t len, bool final) {
String webResponse;
boolean error = false,isFileExists = false;
if (!index) {
if (!filename.startsWith("/"))
filename = "/" + filename;
if (LITTLEFS.exists(filename)) {
isFileExists = true;
// Save the file to a temporary String if it success we continue.
if( saveFileToTemp(filename) ){
LITTLEFS.remove(filename);
}else{
// If the file save was fail we abort everything.
webResponse = "File NOT Uploaded " + filename;
final = true;
error = true;
}
}
if( !error ){
uploadFile = LITTLEFS.open(filename, "w");
}
}
if( !error ){
// Copy content to the actual file
for (size_t i = 0; i < len; i++) {
uploadFile.write(data[i]);
}
}
if (final) {
uploadFile.close();
if( !error ){
if( matchFileSizes(filename,isFileExists) ){
if(filename == "/myHomeProgram.json"){initProgram = true;}
webResponse = "File Uploaded " + filename;
}else{
error = true;
webResponse = "File length mismatch";
}
}
if( error ){
revertBackFile(filename);
}
Serial.println(webResponse);
AsyncWebServerResponse *response = request->beginResponse(200, "text/plain", webResponse);
response->addHeader("Access-Control-Allow-Origin","*");
request->send(response);
}
}