项目场景:
最近的项目中,需要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 }