[Flutter] FTPConnect:Could not start Passive Mode:500 Invalid EPSV Command

项目场景:

最近的项目中,需要APP能够同云端FTP和本地设备的FTP完成文件下载同步操作。


问题描述

在使用的时候,同云端的FTP的下载上传操作可以正常进行,但是同本地FTP服务器的却无法进行,会报无法识别命令的异常。


原因分析:

我所使用的FTPConnect版本为1.0.1,而FTPConnect在1.0.0版本就提供了对于IPv6的支持。
以下载FileDownload为例,我们可以看到其中下载文件的方法中都含有参数supportIPV6,且其默认值为true。

  Future<bool> downloadFile(
    String? sRemoteName,
    File fLocalFile, {
    FileProgress? onProgress,
    bool? supportIPV6 = true,
  }) async {
    _log.log('Download $sRemoteName to ${fLocalFile.path}');
    //check for file existence and init totalData to receive
    int fileSize = 0;
    fileSize = await FTPFile(_socket).size(sRemoteName);
    if (fileSize == -1) {
      throw FTPException('Remote File $sRemoteName does not exist!');
    }

    // Transfer Mode
    await _socket!.setTransferMode(_mode);

    // Enter passive mode
    var response = await TransferUtil.enterPassiveMode(_socket!, supportIPV6);

    //the response will be the file, witch will be loaded with another socket
    await _socket!.sendCommand('RETR $sRemoteName', waitResponse: false);

    // Data Transfer Socket
    int iPort = TransferUtil.parsePort(response, supportIPV6)!;
    _log.log('Opening DataSocket to Port $iPort');
    final Socket dataSocket = await Socket.connect(_socket!.host, iPort,
        timeout: Duration(seconds: _socket!.timeout));
    // Test if second socket connection accepted or not
    response = await TransferUtil.checkIsConnectionAccepted(_socket!);

    // Changed to listen mode instead so that it's possible to send information back on downloaded amount
    var sink = fLocalFile.openWrite(mode: FileMode.writeOnly);
    _log.log('Start downloading...');
    var received = 0;
    await dataSocket.listen((data) {
      sink.add(data);
      if (onProgress != null) {
        received += data.length;
        var percent = ((received / fileSize) * 100).toStringAsFixed(2);
        //in case that the file size is 0, then pass directly 100
        double percentVal = double.tryParse(percent) ?? 100;
        if (percentVal.isInfinite || percentVal.isNaN) percentVal = 100;
        onProgress(percentVal, received, fileSize);
      }
    }).asFuture();

    await dataSocket.close();
    await sink.flush();
    await sink.close();

    //Test if All data are well transferred
    await TransferUtil.checkTransferOK(_socket, response);

    _log.log('File Downloaded!');
    return true;
  }

而在该方法中,有很多地方都用到了supportIPV6这个参数,比如TransferUtil.enterPassiveMode方法中,就需要该参数进行比较,来决定所需要发送的命令。

  ///Tell the socket [socket] that we will enter in passive mode
  static Future<String> enterPassiveMode(
      FTPSocket socket, bool? supportIPV6) async {
    var res = await socket.sendCommand(supportIPV6 == false ? 'PASV' : 'EPSV');
    if (!isResponseStartsWith(res, [229, 227, 150])) {
      throw FTPException('Could not start Passive Mode', res);
    }
    return res;
  }

而ESPV是针对IPv6对FTP进行的扩展,而我们的报错信息就是说500 Invalid EPSV Command,所以可以得出我们本地的FTP不支持IPV6的命令。


解决方案:

我们可以在调用FTPConnect的响应方法的时候,设置supprotIPV6:false即可。(我觉得我都没有写的必要,除了我还有谁会搞错呢)。
除此之外,还需要注意的是FTPConnect中有些方法如:downloadDirectory()等并没有提供supportIPV6参数,所以会默认使用IPV6的指令来运行。如果需要该部分功能的话可以自己编码实现。

如下为我自己重写的一个FTPConnect(通过构造方法设置supportIPV6属性,减少调用FTPConnect对象的方法时,重复的设置参数):

import 'dart:io';

import 'package:archive/archive_io.dart';
import 'package:ftpconnect/src/commands/file.dart';
import 'package:ftpconnect/src/commands/fileDownload.dart';
import 'package:ftpconnect/src/commands/fileUpload.dart';
import 'package:ftpconnect/src/debug/debugLog.dart';
import 'package:ftpconnect/src/debug/noopLog.dart';
import 'package:ftpconnect/src/debug/printLog.dart';
import 'package:ftpconnect/src/util/transferUtil.dart';
import 'package:path/path.dart';

import 'commands/directory.dart';
import 'dto/FTPEntry.dart';
import 'ftpExceptions.dart';
import 'ftpSocket.dart';

class FTPConnect {
  final String _user;
  final String _pass;
  late FTPSocket _socket;
  final FTPDebugLogger _log;
  final bool _supportIPV6;

  /// Create a FTP Client instance
  ///
  /// [host]: Hostname or IP Address
  /// [port]: Port number (Defaults to 21)
  /// [user]: Username (Defaults to anonymous)
  /// [pass]: Password if not anonymous login
  /// [debug]: Enable Debug Logging
  /// [timeout]: Timeout in seconds to wait for responses
  FTPConnect(String host,
      {int port = 21,
      String user = 'anonymous',
      String pass = '',
      bool supportIPV6 = false,
      bool debug = false,
      bool isSecured = false,
      FTPDebugLogger? logger,
      int timeout = 30})
      : _user = user,
        _pass = pass,
        _log = logger != null ? logger : (debug ? PrintLog() : NoOpLogger()),
        _supportIPV6 = supportIPV6 {
    _socket = FTPSocket(host, port, isSecured, _log, timeout);
  }

  /// Connect to the FTP Server
  /// return true if we are connected successfully
  Future<bool> connect() => _socket.connect(_user, _pass);

  /// Disconnect from the FTP Server
  /// return true if we are disconnected successfully
  Future<bool> disconnect() => _socket.disconnect();

  /// Upload the File [fFile] to the current directory
  Future<bool> uploadFile(
    File fFile, {
    String sRemoteName = '',
    TransferMode mode = TransferMode.binary,
    FileProgress? onProgress,
    bool checkTransfer = true,
  }) {
    return FileUpload(_socket, mode, _log).uploadFile(
      fFile,
      remoteName: sRemoteName,
      onProgress: onProgress,
      supportIPV6: _supportIPV6,
      checkTransfer: checkTransfer,
    );
  }

  /// Download the Remote File [sRemoteName] to the local File [fFile]
  Future<bool> downloadFile(
    String? sRemoteName,
    File fFile, {
    TransferMode mode = TransferMode.binary,
    FileProgress? onProgress,
  }) {
    return FileDownload(_socket, mode, _log).downloadFile(sRemoteName, fFile,
        onProgress: onProgress, supportIPV6: _supportIPV6);
  }

  /// Create a new Directory with the Name of [sDirectory] in the current directory.
  ///
  /// Returns `true` if the directory was created successfully
  /// Returns `false` if the directory could not be created or already exists
  Future<bool> makeDirectory(String sDirectory) {
    return FTPDirectory(_socket).makeDirectory(sDirectory);
  }

  /// Deletes the Directory with the Name of [sDirectory] in the current directory.
  ///
  /// Returns `true` if the directory was deleted successfully
  /// Returns `false` if the directory could not be deleted or does not nexist
  Future<bool> deleteEmptyDirectory(String? sDirectory) {
    return FTPDirectory(_socket).deleteEmptyDirectory(sDirectory);
  }

  /// Deletes the Directory with the Name of [sDirectory] in the current directory.
  ///
  /// Returns `true` if the directory was deleted successfully
  /// Returns `false` if the directory could not be deleted or does not nexist
  /// THIS USEFUL TO DELETE NON EMPTY DIRECTORY
  Future<bool> deleteDirectory(String? sDirectory,
      {DIR_LIST_COMMAND cmd = DIR_LIST_COMMAND.MLSD}) async {
    String currentDir = await this.currentDirectory();
    if (!await this.changeDirectory(sDirectory)) {
      throw FTPException("Couldn't change directory to $sDirectory");
    }
    List<FTPEntry> dirContent = await this.listDirectoryContent(cmd: cmd);
    await Future.forEach(dirContent, (FTPEntry entry) async {
      if (entry.type == FTPEntryType.FILE) {
        if (!await deleteFile(entry.name)) {
          throw FTPException("Couldn't delete file ${entry.name}");
        }
      } else {
        if (!await deleteDirectory(entry.name, cmd: cmd)) {
          throw FTPException("Couldn't delete folder ${entry.name}");
        }
      }
    });
    await this.changeDirectory(currentDir);
    return await deleteEmptyDirectory(sDirectory);
  }

  /// Change into the Directory with the Name of [sDirectory] within the current directory.
  ///
  /// Use `..` to navigate back
  /// Returns `true` if the directory was changed successfully
  /// Returns `false` if the directory could not be changed (does not exist, no permissions or another error)
  Future<bool> changeDirectory(String? sDirectory) {
    return FTPDirectory(_socket).changeDirectory(sDirectory);
  }

  /// Returns the current directory
  Future<String> currentDirectory() {
    return FTPDirectory(_socket).currentDirectory();
  }

  /// Returns the content of the current directory
  /// [cmd] refer to the used command for the server, there is servers working
  /// with MLSD and other with LIST
  Future<List<FTPEntry>> listDirectoryContent({
    DIR_LIST_COMMAND? cmd,
  }) {
    return FTPDirectory(_socket)
        .listDirectoryContent(cmd: cmd, supportIPV6: _supportIPV6);
  }

  /// Returns the content names of the current directory
  /// [cmd] refer to the used command for the server, there is servers working
  /// with MLSD and other with LIST for detailed content
  Future<List<String>> listDirectoryContentOnlyNames() {
    return FTPDirectory(_socket)
        .listDirectoryContentOnlyNames(supportIPV6: _supportIPV6);
  }

  /// Rename a file (or directory) from [sOldName] to [sNewName]
  Future<bool> rename(String sOldName, String sNewName) {
    return FTPFile(_socket).rename(sOldName, sNewName);
  }

  /// Delete the file [sFilename] from the server
  Future<bool> deleteFile(String? sFilename) {
    return FTPFile(_socket).delete(sFilename);
  }

  /// check the existence of  the file [sFilename] from the server
  Future<bool> existFile(String sFilename) {
    return FTPFile(_socket).exist(sFilename);
  }

  /// returns the file [sFilename] size from server,
  /// returns -1 if file does not exist
  Future<int> sizeFile(String sFilename) {
    return FTPFile(_socket).size(sFilename);
  }

  /// Upload the File [fileToUpload] to the current directory
  /// if [pRemoteName] is not setted the remote file will take take the same local name
  /// [pRetryCount] number of attempts
  ///
  /// this strategy can be used when we don't need to go step by step
  /// (connect -> upload -> disconnect) or there is a need for a number of attemps
  /// in case of a poor connexion for example
  Future<bool> uploadFileWithRetry(
    File fileToUpload, {
    String pRemoteName = '',
    int pRetryCount = 1,
    FileProgress? onProgress,
  }) {
    Future<bool> uploadFileRetry() async {
      bool res = await this.uploadFile(
        fileToUpload,
        sRemoteName: pRemoteName,
        onProgress: onProgress,
      );
      return res;
    }

    return TransferUtil.retryAction(() => uploadFileRetry(), pRetryCount);
  }

  /// Download the Remote File [pRemoteName] to the local File [pLocalFile]
  /// [pRetryCount] number of attempts
  ///
  /// this strategy can be used when we don't need to go step by step
  /// (connect -> download -> disconnect) or there is a need for a number of attempts
  /// in case of a poor connexion for example
  Future<bool> downloadFileWithRetry(
    String pRemoteName,
    File pLocalFile, {
    int pRetryCount = 1,
    FileProgress? onProgress,
  }) {
    Future<bool> downloadFileRetry() async {
      bool res = await this.downloadFile(
        pRemoteName,
        pLocalFile,
        onProgress: onProgress,
      );
      return res;
    }

    return TransferUtil.retryAction(() => downloadFileRetry(), pRetryCount);
  }

  /// Download the Remote Directory [pRemoteDir] to the local File [pLocalDir]
  /// [pRetryCount] number of attempts
  Future<bool> downloadDirectory(String pRemoteDir, Directory pLocalDir,
      {DIR_LIST_COMMAND? cmd, int pRetryCount = 1}) {
    Future<bool> downloadDir(String? pRemoteDir, Directory pLocalDir) async {
      await pLocalDir.create(recursive: true);

      //read remote directory content
      if (!await this.changeDirectory(pRemoteDir)) {
        throw FTPException('Cannot download directory',
            '$pRemoteDir not found or inaccessible !');
      }
      List<FTPEntry> dirContent = await this.listDirectoryContent(cmd: cmd);
      await Future.forEach(dirContent, (FTPEntry entry) async {
        if (entry.type == FTPEntryType.FILE) {
          File localFile = File(join(pLocalDir.path, entry.name));
          await downloadFile(entry.name!, localFile);
        } else if (entry.type == FTPEntryType.DIR) {
          //create a local directory
          var localDir = await Directory(join(pLocalDir.path, entry.name))
              .create(recursive: true);
          await downloadDir(entry.name, localDir);
          //back to current folder
          await this.changeDirectory('..');
        }
      });
      return true;
    }

    Future<bool> downloadDirRetry() async {
      bool res = await downloadDir(pRemoteDir, pLocalDir);
      return res;
    }

    return TransferUtil.retryAction(() => downloadDirRetry(), pRetryCount);
  }

  /// check the existence of the Directory with the Name of [pDirectory].
  ///
  /// Returns `true` if the directory was changed successfully
  /// Returns `false` if the directory could not be changed (does not exist, no permissions or another error)
  Future<bool> checkFolderExistence(String pDirectory) {
    return this.changeDirectory(pDirectory);
  }

  /// Create a new Directory with the Name of [pDirectory] in the current directory if it does not exist.
  ///
  /// Returns `true` if the directory exists or was created successfully
  /// Returns `false` if the directory not found and could not be created
  Future<bool> createFolderIfNotExist(String pDirectory) async {
    if (!await checkFolderExistence(pDirectory)) {
      return this.makeDirectory(pDirectory);
    }
    return true;
  }

  ///Function that compress list of files and directories into a Zip file
  ///Return true if files compression is finished successfully
  ///[paths] list of files and directories paths to be compressed into a Zip file
  ///[destinationZipFile] full path of destination zip file
  static Future<bool> zipFiles(
      List<String> paths, String destinationZipFile) async {
    var encoder = ZipFileEncoder();
    encoder.create(destinationZipFile);
    for (String path in paths) {
      FileSystemEntityType type = await FileSystemEntity.type(path);
      if (type == FileSystemEntityType.directory) {
        encoder.addDirectory(Directory(path));
      } else if (type == FileSystemEntityType.file) {
        encoder.addFile(File(path));
      }
    }
    encoder.close();
    return true;
  }

  ///Function that unZip a zip file and returns the decompressed files/directories path
  ///[zipFile] file to decompress
  ///[destinationPath] local directory path where the zip file will be extracted
  ///[password] optional: use password if the zip is crypted
  static Future<List<String>> unZipFile(File zipFile, String destinationPath,
      {password}) async {
    //path should ends with '/'
    if (!destinationPath.endsWith('/')) destinationPath += '/';
    //list that will be returned with extracted paths
    final List<String> lPaths = [];

    // Read the Zip file from disk.
    final bytes = await zipFile.readAsBytes();

    // Decode the Zip file
    final archive = ZipDecoder().decodeBytes(bytes, password: password);

    // Extract the contents of the Zip archive to disk.
    for (final file in archive) {
      final filename = file.name;
      if (file.isFile) {
        final data = file.content as List<int>;
        final File f = File(destinationPath + filename);
        await f.create(recursive: true);
        await f.writeAsBytes(data);
        lPaths.add(f.path);
      } else {
        final Directory dir = Directory(destinationPath + filename);
        await dir.create(recursive: true);
        lPaths.add(dir.path);
      }
    }
    return lPaths;
  }
}

///Note that [LIST] and [MLSD] return content detailed
///BUT [NLST] return only dir/file names inside the given directory
enum DIR_LIST_COMMAND { NLST, LIST, MLSD }
enum TransferMode { ascii, binary }

个人博客


版权声明:本文为qq_42943623原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。