ESP8266WebServerのSPIFFSファイルビューアに簡易FTPクライアントを追加する 

最近ESP32でFTPクライアントソフトが必要になり、GitHubでダウンロードして
動作確認できました。　しかしそのソフトはESP8266へは移植作業が必要でした。
ソース内容を確認するとデータ操作にて、非互換命令を使っている箇所があり
無駄な改造になりそうだったので、大改造ついでにESP8266のSPIFFSファイルビューワに
簡易FTP機能を追加することにしました。


#ESP8266環境で必要なリソース
　SPIFFSを使います。

#ESP32で使うには？
  ESP8266WebServerの移植が必要になります。(確認済)
  私の過去の日記に移植内容を書いてあります。 ※数行治す程度です。

　ESP8266とESP32はSPIFFSのopenの引数設定が違うので修正します。
  "SD_MMC.h"も同程度の修正で動作します。

#機能
・WindowsのGoogle ChromeよりIPアドレス/files.htmlへアクセスすることで
　=>ファイル一覧が確認できる。
　=>任意のファイルを削除できる。
　=>任意のファイルの内容をテキスト表示できる。
　=>任意のファイルをダウンロードできる。
　=>Windows上の任意のファイルをアップロードできる。
　=>任意のFTPサーバ/フォルダに任意のファイルをアップロードできる。

・WindowsのGoogle ChromeよりIPアドレス/ftp.htmlへアクセスすることで
　=>FTPサーバにある任意フォルダのファイル一覧が確認できる。
　=>任意のファイルをESP8266のSPIFFSにダウンロードできる。



メインソースはこんな風にします。

```ino:esp8266FILER.ino:

#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ESP8266WebServer.h>
#include <FS.h>

// WiFi接続先
const char* ssid = "guest";                 // SSID
const char* password = "guest";             // PASSWORD
// FTP接続先などの設定
const char *ftpserver = "192.168.2.100";    // 接続先
const char *ftpuser = "ftpuser";            // ユーザ名
const char *ftppassword = "ftpuser";        // パスワード
const char *ftppath = "/folder/folder/";    // アクセスパス
uint16_t FTPtimeout = 1000;
char FTPoutBuf[128];
char FTPoutCount;

ESP8266WebServer server(80);
// HTMLヘッダ
const char *ESPHTMLHeader ="<html lang=\"ja\">\n<head>\n\
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=Shift_JIS\">\n\
<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n</head>\n";
char *ESPfilename = "filename";

WiFiClient client;
//------------------------------------------------------------------------------------------------------
bool FTPDisconnect( ) {
byte thisByte = 0;
  client.println( "QUIT" );
  while (!client.available()) delay(1);
  while (client.available()) {
    thisByte = client.read();
    Serial.write(thisByte);
  }
  client.stop();
  Serial.println( "FTP disconnected" );
}
//------------------------------------------------------------------------------------------------------
bool FTPReceiveCheck() {
  byte returnCode;
  byte thisByte;
  while (!client.available()) delay(1);
  returnCode = client.peek();
  FTPoutCount = 0;
  while (client.available()) {
    FTPoutBuf[FTPoutCount] = client.read();
    if (FTPoutCount < (sizeof(FTPoutBuf)-1) ) FTPoutCount++;
  }
  FTPoutBuf[FTPoutCount] = 0;
  Serial.print( FTPoutBuf );
  if (returnCode >= '4') {
    FTPDisconnect();
    return false;
  }
  return true;
}
//------------------------------------------------------------------------------------------------------
bool FTPConnect() {
  if ( !client.connect(ftpserver, 21) ) {  // 21 = FTP server
    Serial.println( "FTP connection failed" );
    return false;
  }
  if (!FTPReceiveCheck()) return false;
  Serial.println( "FTP connected" );
  Serial.println( "Send USER" );
  client.print( "USER " );    client.println( ftpuser );
  if (!FTPReceiveCheck()) return false;
  Serial.println( "Send PASSWORD" );
  client.print( "PASS " );    client.println( ftppassword );
  if (!FTPReceiveCheck()) return false;
  Serial.println( "Send SYST" );
  client.println( "SYST" );
  if (!FTPReceiveCheck()) return false;
  Serial.println( "Send Type I");
  client.println( "Type I" );
  if (!FTPReceiveCheck()) return false;
  Serial.println( "Send PASV");
  client.println( "PASV" );
  if (!FTPReceiveCheck()) return false;
  return true;
}
//------------------------------------------------------------------------------------------------------
int FTPGetHiPort( ) {
char *tStr = strtok(FTPoutBuf, "(,");
int array_pasv[6];
  for ( int i = 0 ; i < 6 ; i++ ) {
    tStr = strtok(NULL, "(,");
    array_pasv[i] = atoi(tStr);
    if (tStr == NULL)  Serial.println("Bad PASV Answer");
  }
  unsigned int hiPort = array_pasv[4] << 8;
  unsigned int loPort = array_pasv[5] & 255;
  hiPort = hiPort | loPort;
  Serial.print("Data port: ");  Serial.println(hiPort);
  return hiPort;
}
//------------------------------------------------------------------------------------------------------
bool FTPList( String dir ) {
  if ( !FTPConnect() ){
    client.stop();
    Serial.println( "FTP connection failed" );
    return false;
  }
  int hiPort = FTPGetHiPort();
  WiFiClient dclient;
  if ( !dclient.connect(ftpserver, hiPort) ) {
    Serial.println( "Data connection failed" );
    client.stop();
    return false;
  }
  Serial.println( "Data connected" );
  client.print( "NLST " );  client.println( dir );
  if (!FTPReceiveCheck()) return false;
  unsigned long _m = millis();
  while( !dclient.available() && millis() < _m + FTPtimeout) delay(1);
  String output = ESPHTMLHeader;
  FSInfo fs_info;
  SPIFFS.info(fs_info);
  output += "FTP FILER\n\
<form name='ftp' action=/ftp.html method='post'>\
<p><select name=\"mode\">\n\
<option value=\"none\" selected>-------</option>\
<option value=\"ftpdownload\">download file</option>\
</select><br>\n";
  while( dclient.available() ) {
    String filename = String(dclient.readStringUntil('\n')).substring(1);
    output += "<input type=\"radio\" name=\"remove\" value=\"";
    output += filename;
    output += "\">";
    output += filename;
    output += "\n<br>";
    Serial.println( filename );
  }
  output += "<input type=submit value=OK target=tif>\n</form>\n</html>\n";
  server.send(200, "text/html", output);
  dclient.stop();
  Serial.println( "Data disconnected" );
  if (!FTPReceiveCheck()) return false;
  return FTPDisconnect();
}
//------------------------------------------------------------------------------------------------------
// 選択したファイルをＳＰＩＦＦＳにダウンロードする
bool FTPdownload( ){
  String path = server.arg(1).substring( 0, server.arg(1).indexOf( '\xd', 1 ) );  // なぜか末尾に改行らしきゴミが付いているので消す
  String ESPpath;
  int pos = path.lastIndexOf( '/', path.length() );
  if( pos>0 ) ESPpath = path.substring( pos-1 );
  else        ESPpath = "/" + path;
  path = "/" + server.arg(1);
  Serial.print( "FTP download file : " );   Serial.println( path );
  Serial.print( "ESP filename : " );        Serial.println( ESPpath );
  SPIFFS.remove( ESPpath );
  File fh = SPIFFS.open( ESPpath, "w" );
  if (!fh) {
    Serial.println( "SPIFFS create file failed" );
    return false;
  }
  if ( !FTPConnect() ){
    client.stop();
    Serial.println( "FTP connection failed" );
    return false;
  }
  int hiPort = FTPGetHiPort();
  WiFiClient dclient;
  if ( !dclient.connect(ftpserver, hiPort) ) {
    Serial.println( "Data connection failed" );
    client.stop();
    fh.close();
    SPIFFS.remove( ESPpath );
    return false;
  }
  Serial.println( "Data connected" );
  client.print( "RETR " );  client.println( path );
  if ( !FTPReceiveCheck()) {
    dclient.stop();
    fh.close();
    SPIFFS.remove(path);
    Serial.println( "RETR error" );
    return false;
  }
  Serial.println( "RETR file" );
  #define bufSizeFTP 1460
  uint8_t clientBuf[bufSizeFTP];
  size_t clientCount = 0;  
  while (dclient.connected()) {
    while (dclient.available()) {
      clientBuf[clientCount] = dclient.read();
      clientCount++;
      if (clientCount > (bufSizeFTP - 1)) {
        fh.write((const uint8_t *) &clientBuf[0], bufSizeFTP);
        clientCount = 0;
        Serial.print( "." );
      }
    }
  }
  if (clientCount > 0)
    fh.write((const uint8_t *) &clientBuf[0], clientCount);
  Serial.println( "end" );
  fh.close();
  dclient.stop();
  Serial.println( "Data disconnected" );
  return FTPDisconnect();
}
//=====================================================================================
// ＳＰＩＦＦＳで選択したファイルをＦＴＰサーバーにアップロードする
bool ESPFTPupload( ){
  String path = server.arg(1).substring( 0, server.arg(1).indexOf( '\xd', 1 ) );  // なぜか末尾に改行らしきゴミが付いているので消す;
  String FTPpath = ftppath + server.arg(1);
  path = "/" + path;
  Serial.print( "ESPfile upload : " );  Serial.println( path );
  Serial.print( "FTP file path : " );    Serial.println( FTPpath );
  if (!SPIFFS.exists( path.c_str() )) {
    Serial.println( "SPIFFS file not found" );
    return false;
  }
  if ( !FTPConnect() ){
    client.stop();
    Serial.println( "FTP connection failed" );
    return false;
  }
  int hiPort = FTPGetHiPort();
  WiFiClient dclient;
  if ( !dclient.connect(ftpserver, hiPort) ) {
    Serial.println( "Data connection failed" );
    client.stop();
    return false;
  }
  Serial.println( "Data connected" );
  Serial.println( "Send STOR filename" );
  client.print( "STOR " );  client.println( FTPpath );
  if (!FTPReceiveCheck()) {
    Serial.println( "STOR command failed" );
    dclient.stop();
    return false;
  }
  File fh = SPIFFS.open( path.c_str(), "r" );
  if (!fh) {
    Serial.println( "SPIFFS file open fail" );
    return false;
  }
  if (!fh.seek((uint32_t)0, SeekSet)) {
    Serial.println( "SPIFFS file Rewind fail" );
    fh.close();
    return false;
  }
  Serial.println( "Writing file" );
  // for faster upload increase buffer size to 1460
  //#define bufSizeFTP 64
  #define bufSizeFTP 1460
  uint8_t clientBuf[bufSizeFTP];
  size_t clientCount = 0;  
  while (fh.available()) {
    clientBuf[clientCount] = fh.read();
    clientCount++;
    if (clientCount > (bufSizeFTP - 1)) {
      dclient.write((const uint8_t *) &clientBuf[0], bufSizeFTP);
      clientCount = 0;
      Serial.print( "." );
    }
  }
  if (clientCount > 0) dclient.write((const uint8_t *) &clientBuf[0], clientCount);
  fh.close();
  dclient.stop();
  Serial.println( "Data disconnected" );
  return FTPDisconnect();
}
//=====================================================================================
// 選択ファイルをテキストで表示する
void ESPlist(){
  Serial.print( "display text : " );  Serial.println( server.arg(1) );
  String dataType = "text/plain";
  String path = "/" + server.arg(1);
  File dataFile = SPIFFS.open( path, "r");
  if (server.streamFile(dataFile, dataType) != dataFile.size())
    Serial.println( "Sent less data than expected!" );
  dataFile.close();
}
//=====================================================================================
// 選択ファイルを起動する(htmlなら普通に表示
// ※※※拡張子に従って実行する処理を書きます※※※
void ESPexec(){
  Serial.print( "execute file : " );  Serial.println( server.arg(1) );
  String path = "/" + server.arg(1);
  //loadFromDataFile( path );     
}
//=====================================================================================
// 削除ファイルを実際に削除する
void ESPremove(){
  String path = "/" + server.arg(1);
  SPIFFS.remove(path);
  Serial.print( "removed file : " );  Serial.println( server.arg(1) );
}
//=====================================================================================
// アップロード用htmlを表示する
void ESPuploadhtml( ){
  Serial.println( "execute : upload.html" );
  String output = ESPHTMLHeader;
  output += "\
<form method=\"POST\" action=\"/upload\" enctype=\"multipart/form-data\">\n\
<p>Select upload file</p>\n<input type=\"file\" name=\"file\"/><br>\n\
<input type=\"submit\" value=\"SUBMIT\">\n</form>\n</html>\n";
  server.send(200, "text/html", output);
}
//=====================================================================================
// ファイル一覧及び削除ファイルの選択
void ESPfiles( ){
  if( 0<server.args() ){
    if( server.arg(0)=="remove" )         ESPremove();
    if( server.arg(0)=="list" ){          ESPlist();          return; }
    if( server.arg(0)=="exec" ){          ESPexec();          return; }
    if( server.arg(0)=="/upload.html" ){  ESPuploadhtml();    return; }
    if( server.arg(0)=="ftpupload" ){
      if( ESPFTPupload( ) ){              FTPList( ftppath ); return; }
    }
  }
  String output = ESPHTMLHeader;
  FSInfo fs_info;
  SPIFFS.info(fs_info);
  output += "SPIFFS FILER<br>total:" + String(fs_info.totalBytes/1024);
  output += " Kbytes  /  " + String((fs_info.totalBytes - fs_info.usedBytes)/1024);
  output += " Kbytes free\n<br>\
<form name='files' action=/files.html method='post'>\
<p><select name=\"mode\">\n\
<option value=\"exec\" selected>exec file</option>\
<option value=\"list\">source list</option>\
<option value=\"/upload.html\">upload file</option>\
<option value=\"remove\">removed file</option>\
<option value=\"ftpupload\">FTP upload</option>\
</select><br>\n";
  Dir dir = SPIFFS.openDir( server.arg("dir") );
  while(dir.next()){
    File entry = dir.openFile("r");
    output += "<input type=\"radio\" name=\"remove\" value=\"";
    output += String(entry.name()).substring(1);
    output += "\">";
    output += String(entry.name()).substring(1);
    output += " : ";
    if( (dir.fileSize()/1024)>9 ){ output += String(dir.fileSize()/1024);  output += " Kbytes\n<br>";
    }else{                         output += String(dir.fileSize());       output += " bytes\n<br>";    }
    entry.close();
  }
  output += "<input type=submit value=OK target=tif>\n</form>\n</html>\n";
  server.send(200, "text/html", output);
}
//=====================================================================================
// upload.htmlで選択したファイルをアップロードする
File fsUploadFile;
void ESPhandleFileUpload(){
  HTTPUpload& upload = server.upload();
  if ( upload.status == UPLOAD_FILE_START){
    String filename = "/" + upload.filename;
    Serial.print( "upload [" );
    Serial.print( filename );
    Serial.print( "] start:");
    fsUploadFile = SPIFFS.open( filename, "w");
  } else if ( upload.status == UPLOAD_FILE_WRITE){
    if ( fsUploadFile)
      fsUploadFile.write(upload.buf, upload.currentSize);
    Serial.print( upload.currentSize );
    Serial.print( ":" );
  } else if( upload.status == UPLOAD_FILE_END){
    if ( fsUploadFile )
      fsUploadFile.close();
    Serial.println( "end" );
  }
}
//=====================================================================================
// ftp.htmlの処理
void ESPftp( ){
  if( 0<server.args() ){
    if( server.arg(0)=="ftpdownload" ){
      if( FTPdownload()==true ){
        ESPfiles();
        return;
      }
    }
  }
  FTPList( ftppath );
}
//=====================================================================================
void setup(void){  
  Serial.begin(115200);
  SPIFFS.begin();
  
  // Connect to WiFi network
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");  
  
  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");  Serial.println(ssid);
  Serial.print("IP address: ");   Serial.println(WiFi.localIP());

  if (!MDNS.begin("esp8266")) {
    Serial.println("Error setting up MDNS responder!");
    while(1) { 
      delay(1000);
    }
  }
  // Start TCP (HTTP) server
  server.begin();
  Serial.println("web server started");
  // Add service to MDNS-SD
  MDNS.addService("http", "tcp", 80);
  //
  server.on("/upload", HTTP_POST, [](){ ESPfiles(); }, ESPhandleFileUpload);
  server.on("/files.html",  ESPfiles);
  server.on("/upload.html", ESPuploadhtml);
  server.on("/ftp.html",    ESPftp);
}
//=====================================================================================
void loop(void){
  server.handleClient( );
  MDNS.update();
}

```

あとがき
　FTP側もファイラーで作ろうかと思ったのですが、用意するFTPサーバで
全権限持ったユーザーを開放させるのは「無謀」かな？と思ったので
Upload/Downloadを作ってファイル内容の確認を行って良しとしました。

私の使い方だと以下の３機能あれば十分ですかね・・・？
・接続チェックでファイル一覧が見たい
・年・月日のフォルダを作成したい
・年月日時分秒のファイルを書き込みたい