[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的指令来运行。如果需要该部分功能的话可以自己编码实现 (新版本提供了set supportIPV6方法)。

如下为我自己重写的一个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 }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
"Could not start audio source"(无法启动音频源)是指在尝试启动一个音频来源时遇到了问题。以下是可能导致这个问题的几种原因和解决方法。 1. 硬件问题:首先要确保音频设备的物理连接正常。检查音频线缆是否正确连接到计算机或设备。如果使用的是外部设备,如麦克风或扬声器,请确保其已正确连接并可以工作。尝试重新插拔插头来重新建立连接。 2. 音频驱动程序问题:音频驱动程序是用于控制和管理计算机中的音频设备的软件。如果驱动程序过时、损坏或不兼容,可能会导致无法启动音频源。在这种情况下,尝试更新或重新安装音频驱动程序。可以通过到音频设备制造商的官方网站下载最新的驱动程序,并按照说明进行安装。 3. 软件设置问题:有时音频源无法启动是由于软件设置的问题。检查系统的音频设置,确保选择正确的音频输入和输出设备。在Windows操作系统中,可以通过控制面板或设置菜单找到音频设置。在音频输入和输出选项卡中选择正确的设备。 4. 冲突应用程序:有时其他应用程序可能会占用音频设备,导致无法启动音频源。关闭或退出其他正在运行的音频相关应用程序,然后重新启动音频源。如有必要,可以尝试重启计算机。 以上是可能导致"Could not start audio source"的几种常见原因和解决方法。如果问题仍然存在,可能需要进一步检查音频硬件或咨询技术支持以获取更多帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Elkszero

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值