Browse Source

feat: 添加FTP连接工具库实现文件上传下载等功能

weicu
lxm 1 year ago
parent
commit
52c174d9cb
  1. 6
      lib/core/utils/ftpconnect/ftpconnect.dart
  2. 98
      lib/core/utils/ftpconnect/src/commands/directory.dart
  3. 190
      lib/core/utils/ftpconnect/src/commands/file.dart
  4. 265
      lib/core/utils/ftpconnect/src/ftp_entry.dart
  5. 11
      lib/core/utils/ftpconnect/src/ftp_exceptions.dart
  6. 24
      lib/core/utils/ftpconnect/src/ftp_reply.dart
  7. 221
      lib/core/utils/ftpconnect/src/ftp_socket.dart
  8. 307
      lib/core/utils/ftpconnect/src/ftpconnect_base.dart
  9. 13
      lib/core/utils/ftpconnect/src/logger.dart
  10. 57
      lib/core/utils/ftpconnect/src/utils.dart

6
lib/core/utils/ftpconnect/ftpconnect.dart

@ -0,0 +1,6 @@
library ftpconnect;
export 'src/ftp_exceptions.dart';
export 'src/ftpconnect_base.dart';
export 'src/ftp_entry.dart';
export 'src/logger.dart';

98
lib/core/utils/ftpconnect/src/commands/directory.dart

@ -0,0 +1,98 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import '../ftp_entry.dart';
import '../ftp_exceptions.dart';
import '../ftp_reply.dart';
import '../ftp_socket.dart';
import '../ftpconnect_base.dart';
import '../utils.dart';
class FTPDirectory {
final FTPSocket _socket;
FTPDirectory(this._socket);
Future<bool> makeDirectory(String sName) async {
FTPReply sResponse = await (_socket.sendCommand('MKD $sName'));
return sResponse.isSuccessCode();
}
Future<bool> deleteEmptyDirectory(String? sName) async {
FTPReply sResponse = await (_socket.sendCommand('rmd $sName'));
return sResponse.isSuccessCode();
}
Future<bool> changeDirectory(String? sName) async {
FTPReply sResponse = await (_socket.sendCommand('CWD $sName'));
return sResponse.isSuccessCode();
}
Future<String> currentDirectory() async {
FTPReply sResponse = await _socket.sendCommand('PWD');
if (!sResponse.isSuccessCode()) {
throw FTPConnectException(
'Failed to get current working directory', sResponse.message);
}
int iStart = sResponse.message.indexOf('"') + 1;
int iEnd = sResponse.message.lastIndexOf('"');
return sResponse.message.substring(iStart, iEnd);
}
Future<List<FTPEntry>> directoryContent() async {
// Enter passive mode
FTPReply response = await _socket.openDataTransferChannel();
// Directoy content listing, the response will be handled by another socket
_socket.sendCommandWithoutWaitingResponse(_socket.listCommand.describeEnum);
// Data transfer socket
int iPort = Utils.parsePort(response.message, _socket.supportIPV6);
Socket dataSocket = await Socket.connect(_socket.host, iPort,
timeout: Duration(seconds: _socket.timeout));
//Test if second socket connection accepted or not
response = await _socket.readResponse();
//some server return two lines 125 and 226 for transfer finished
bool isTransferCompleted = response.isSuccessCode();
if (!isTransferCompleted && response.code != 125 && response.code != 150) {
throw FTPConnectException('Connection refused. ', response.message);
}
List<int> lstDirectoryListing = [];
await dataSocket.listen((Uint8List data) {
lstDirectoryListing.addAll(data);
}).asFuture();
await dataSocket.close();
if (!isTransferCompleted) {
response = await _socket.readResponse();
if (!response.isSuccessCode()) {
throw FTPConnectException('Transfer Error.', response.message);
}
}
// Convert MLSD response into FTPEntry
List<FTPEntry> lstFTPEntries = <FTPEntry>[];
String.fromCharCodes(lstDirectoryListing).split('\n').forEach((line) {
if (line.trim().isNotEmpty) {
lstFTPEntries.add(
FTPEntry.parse(line.replaceAll('\r', ""), _socket.listCommand),
);
}
});
return lstFTPEntries;
}
Future<List<String>> directoryContentNames() async {
var list = await directoryContent();
return list.map((f) => f.name).whereType<String>().toList();
}
}

190
lib/core/utils/ftpconnect/src/commands/file.dart

@ -0,0 +1,190 @@
import 'dart:async';
import 'dart:io';
import 'package:path/path.dart';
import '../../ftpconnect.dart';
import '../ftp_reply.dart';
import '../ftp_socket.dart';
import '../utils.dart';
typedef FileProgress = void Function(
double progressInPercent, int totalReceived, int fileSize);
class FTPFile {
final FTPSocket _socket;
FTPFile(this._socket);
Future<bool> rename(String sOldName, String sNewName) async {
FTPReply sResponse = await (_socket.sendCommand('RNFR $sOldName'));
if (sResponse.code != 350) {
return false;
}
sResponse = await (_socket.sendCommand('RNTO $sNewName'));
return sResponse.isSuccessCode();
}
Future<bool> delete(String? sFilename) async {
FTPReply sResponse = await (_socket.sendCommand('DELE $sFilename'));
return sResponse.isSuccessCode();
}
Future<bool> exist(String sFilename) async {
return await size(sFilename) != -1;
}
Future<int> size(String? sFilename) async {
try {
FTPReply sResponse = await (_socket.sendCommand('SIZE $sFilename'));
if (!sResponse.isSuccessCode() &&
_socket.transferType != TransferType.binary) {
//check if ascii mode get refused
//change to binary mode if ascii mode refused
final _socketTransferTypeBackup = _socket.transferType;
await _socket.setTransferType(TransferType.binary);
sResponse = await (_socket.sendCommand('SIZE $sFilename'));
//back to default mode
await _socket.setTransferType(_socketTransferTypeBackup);
}
return int.parse(sResponse.message.replaceAll('213 ', ''));
} catch (e) {
return -1;
}
}
Future<bool> download(
String? sRemoteName,
File fLocalFile, {
FileProgress? onProgress,
}) async {
_socket.logger.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 FTPConnectException('Remote File $sRemoteName does not exist!');
}
// Enter passive mode
FTPReply response = await _socket.openDataTransferChannel();
//the response will be the file, witch will be loaded with another socket
_socket.sendCommandWithoutWaitingResponse('RETR $sRemoteName');
// Data Transfer Socket
int lPort = Utils.parsePort(response.message, _socket.supportIPV6);
_socket.logger.log('Opening DataSocket to Port $lPort');
final Socket dataSocket = await Socket.connect(_socket.host, lPort,
timeout: Duration(seconds: _socket.timeout));
// Test if second socket connection accepted or not
response = await _socket.readResponse();
//some server return two lines 125 and 226 for transfer finished
bool isTransferCompleted = response.isSuccessCode();
if (!isTransferCompleted && response.code != 125 && response.code != 150) {
throw FTPConnectException('Connection refused. ', response.message);
}
// Changed to listen mode instead so that it's possible to send information back on downloaded amount
_socket.logger.log('Start downloading...');
var sink = fLocalFile.openWrite(mode: FileMode.writeOnly);
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();
if (!isTransferCompleted) {
//Test if All data are well transferred
response = await _socket.readResponse();
if (!response.isSuccessCode()) {
throw FTPConnectException('Transfer Error.', response.message);
}
}
_socket.logger.log('File Downloaded!');
return true;
}
/// Upload File [fFile] to the current directory with [remoteName] (using filename if not set)
Future<bool> upload(
File fFile, {
String remoteName = '',
FileProgress? onProgress,
}) async {
_socket.logger.log('Upload File: ${fFile.path}');
// Enter passive mode
FTPReply response = await _socket.openDataTransferChannel();
// Store File
String sFilename = remoteName;
if (sFilename.isEmpty) {
sFilename = basename(fFile.path);
}
// The response is the file to upload, witch will be managed by another socket
_socket.sendCommandWithoutWaitingResponse('STOR $sFilename');
// Data Transfer Socket
int iPort = Utils.parsePort(response.message, _socket.supportIPV6);
_socket.logger.log('Opening DataSocket to Port $iPort');
final Socket dataSocket = await Socket.connect(_socket.host, iPort);
//Test if second socket connection accepted or not
response = await _socket.readResponse();
//some server return two lines 125 and 226 for transfer finished
bool isTransferCompleted = response.isSuccessCode();
if (!isTransferCompleted && response.code != 125 && response.code != 150) {
throw FTPConnectException('Connection refused. ', response.message);
}
_socket.logger.log('Start uploading...');
var received = 0;
int fileSize = await fFile.length();
Stream<List<int>> readStream = fFile.openRead().transform(
StreamTransformer.fromHandlers(
handleData: (data, sink) {
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);
}
},
),
);
await dataSocket.addStream(readStream);
await dataSocket.flush();
await dataSocket.close();
if (!isTransferCompleted) {
// Test if All data are well transferred
response = await _socket.readResponse();
if (!response.isSuccessCode()) {
throw FTPConnectException('Transfer Error.', response.message);
}
}
_socket.logger.log('File Uploaded!');
return true;
}
}

265
lib/core/utils/ftpconnect/src/ftp_entry.dart

@ -0,0 +1,265 @@
import 'package:intl/intl.dart';
import 'ftp_exceptions.dart';
import 'ftpconnect_base.dart';
class FTPEntry {
final String name;
final DateTime? modifyTime;
final String? permission;
final FTPEntryType type;
final int? size;
final String? unique;
final String? group;
final int? gid;
final String? mode;
final String? owner;
final int? uid;
final Map<String, String>? additionalProperties;
static final RegExp regexpLIST = RegExp(r"^([\-ld])" // Directory flag [1]
r"([\-rwxs]{9})\s+" // Permissions [2]
r"(\d+)\s+" // Number of items [3]
r"(\w+)\s+" // File owner [4]
r"(\w+)\s+" // File group [5]
r"(\d+)\s+" // File size in bytes [6]
r"(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+" // date[7]
r"(.+)$" //file/dir name[8]
);
static final regexpLISTSiiServers = RegExp(r"^(.{8}\s+.{7})\s+" //date[1]
r"(.{0,5})\s+" //type file or dir [2]
r"(\d{0,24})\s+" //size [3]
r"(.+)$" //file/ dir name [4]
);
// Hide constructor
FTPEntry._(
this.name,
this.modifyTime,
this.permission,
this.type,
this.size,
this.unique,
this.group,
this.gid,
this.mode,
this.owner,
this.uid,
this.additionalProperties);
factory FTPEntry.parse(String responseLine, ListCommand cmd) {
if (responseLine.trim().isEmpty) {
throw FTPConnectException("Can't parse a null or blank response line");
}
if (cmd == ListCommand.LIST) {
return FTPEntry._parseListCommand(responseLine);
} else if (cmd == ListCommand.NLST) {
return FTPEntry._(responseLine, null, null, FTPEntryType.UNKNOWN, null,
null, null, null, null, null, null, null);
} else {
return FTPEntry._parseMLSDCommand(responseLine);
}
}
factory FTPEntry._parseMLSDCommand(final String responseLine) {
String _name = "";
DateTime? _modifyTime;
String? _permission;
FTPEntryType _type = FTPEntryType.UNKNOWN;
int _size = 0;
String? _unique;
String? _group;
int _gid = -1;
String? _mode;
String? _owner;
int _uid = -1;
Map<String, String> _additional = {};
// Split and trim line
responseLine.trim().split(';').forEach((property) {
final prop = property
.split('=')
.map((part) => part.trim())
.toList(growable: false);
if (prop.length == 1) {
// Name
_name = prop[0];
} else {
// Other attributes
switch (prop[0].toLowerCase()) {
case 'modify':
final String date =
prop[1].substring(0, 8) + 'T' + prop[1].substring(8);
_modifyTime = DateTime.tryParse(prop[1]) ?? DateTime.tryParse(date);
break;
case 'perm':
_permission = prop[1];
break;
case 'size':
_size = int.parse(prop[1]);
break;
case 'type':
if (prop[1] == 'dir') {
_type = FTPEntryType.DIR;
} else if (prop[1] == 'file') {
_type = FTPEntryType.FILE;
} else {
_type = FTPEntryType.LINK;
}
break;
case 'unique':
_unique = prop[1];
break;
case 'unix.group':
_group = prop[1];
break;
case 'unix.gid':
_gid = int.parse(prop[1]);
break;
case 'unix.mode':
_mode = prop[1];
break;
case 'unix.owner':
_owner = prop[1];
break;
case 'unix.uid':
_uid = int.parse(prop[1]);
break;
default:
_additional.putIfAbsent(prop[0], () => prop[1]);
break;
}
}
});
return FTPEntry._(_name, _modifyTime, _permission, _type, _size, _unique,
_group, _gid, _mode, _owner, _uid, Map.unmodifiable(_additional));
}
///reference http://cr.yp.to/ftp/list/binls.html
///-rw-r--r-- 1 owner group 213 Aug 26 16:31 FileName.txt
///d for Dir
///- for file
///
/// SII servers format:
/// 02-11-15 03:05PM <DIR> 1410887680 directory
/// 02-11-15 03:05PM 1410887680 file.avi
factory FTPEntry._parseListCommand(final String responseLine) {
if (regexpLIST.hasMatch(responseLine)) {
return FTPEntry._parseLIST(responseLine);
} else if (regexpLISTSiiServers.hasMatch(responseLine)) {
return FTPEntry._parseLISTiis(responseLine);
} else {
throw FTPConnectException(
'Invalid format <$responseLine> for LIST command response !');
}
}
factory FTPEntry._parseLIST(final String responseLine) {
String _name = "";
DateTime? _modifyTime;
String? _persmission;
FTPEntryType _type = FTPEntryType.UNKNOWN;
int _size = 0;
String? _unique;
String? _group;
int _gid = -1;
String? _mode;
String? _owner;
int _uid = -1;
Iterable<Match> matches = regexpLIST.allMatches(responseLine);
for (Match match in matches) {
if (match.group(1) == "-") {
_type = FTPEntryType.FILE;
} else if (match.group(1) == "d") {
_type = FTPEntryType.DIR;
} else {
_type = FTPEntryType.LINK;
}
//permission
_persmission = match.group(2);
//nb files
//var nbFiles = match.group(3);
//owner
_owner = match.group(4);
//group
_group = match.group(5);
//size
_size = int.tryParse(match.group(6)!) ?? 0;
//date
String date = (match.group(7)!.split(" ")..removeWhere((i) => i.isEmpty))
.join(" "); //keep only one space
//insert year
if (date.contains(':')) date = '$date ${DateTime.now().year}';
var format = date.contains(':') ? 'MMM dd hh:mm yyyy' : 'MMM dd yyyy';
_modifyTime = DateFormat(format, 'en_US').parse(date);
//file/dir name
_name = match.group(8)!;
}
return FTPEntry._(_name, _modifyTime, _persmission, _type, _size, _unique,
_group, _gid, _mode, _owner, _uid, {});
}
factory FTPEntry._parseLISTiis(final String responseLine) {
String _name = "";
DateTime? _modifyTime;
String? _persmission;
FTPEntryType _type = FTPEntryType.UNKNOWN;
int _size = 0;
String? _unique;
String? _group;
int _gid = -1;
String? _mode;
String? _owner;
int _uid = -1;
Iterable<Match> matches = regexpLISTSiiServers.allMatches(responseLine);
for (Match match in matches) {
//date
String date =
match.group(1)!.split(" ").fold('', (previousValue, element) {
//keep only one space and add fullyear if only last 2 digits in year
if (element.isEmpty) return previousValue;
if (previousValue.isEmpty) {
return element.length <= 8
? element.substring(0, 6) +
DateTime.now().year.toString().substring(0, 2) +
element.substring(6, 8)
: element;
}
return '$previousValue $element';
});
_modifyTime = DateFormat('MM-dd-yyyy hh:mma').parse(date);
//type
if (match.group(2)!.trim().isEmpty) {
_type = FTPEntryType.FILE;
} else if (match.group(2)!.toLowerCase().contains("dir")) {
_type = FTPEntryType.DIR;
} else {
_type = FTPEntryType.LINK;
}
//size
_size = int.tryParse(match.group(3)!) ?? 0;
//file/dir name
_name = match.group(4)!;
}
return FTPEntry._(_name, _modifyTime, _persmission, _type, _size, _unique,
_group, _gid, _mode, _owner, _uid, {});
}
@override
String toString() =>
'name=$name;modify=$modifyTime;perm=$permission;type=${type.describeEnum.toLowerCase()};size=$size;unique=$unique;unix.group=$group;unix.mode=$mode;unix.owner=$owner;unix.uid=$uid;unix.gid=$gid';
}
enum FTPEntryType { FILE, DIR, LINK, UNKNOWN }
extension FtpEntryTypeEnum on FTPEntryType {
String get describeEnum =>
this.toString().substring(this.toString().indexOf('.') + 1);
}

11
lib/core/utils/ftpconnect/src/ftp_exceptions.dart

@ -0,0 +1,11 @@
class FTPConnectException implements Exception {
final String message;
final String? response;
FTPConnectException(this.message, [this.response]);
@override
String toString() {
return 'FTPConnectException: $message (Response: $response)';
}
}

24
lib/core/utils/ftpconnect/src/ftp_reply.dart

@ -0,0 +1,24 @@
import 'dart:core';
class FTPReply {
final int _code;
final String _messages;
FTPReply(this._code, this._messages);
get code => this._code;
get message => this._messages;
bool isSuccessCode() {
int aux = this._code - 200;
return aux >= 0 && aux < 100;
}
@override
String toString() {
StringBuffer buffer = new StringBuffer();
buffer.write("FTPReply = [code= $_code, message= $_messages]");
return buffer.toString();
}
}

221
lib/core/utils/ftpconnect/src/ftp_socket.dart

@ -0,0 +1,221 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import '../ftpconnect.dart';
import 'ftp_reply.dart';
class FTPSocket {
final String host;
final int port;
final Logger logger;
final int timeout;
final SecurityType securityType;
late RawSocket _socket;
TransferMode transferMode = TransferMode.passive;
TransferType _transferType = TransferType.auto;
ListCommand listCommand = ListCommand.MLSD;
bool supportIPV6 = false;
FTPSocket(this.host, this.port, this.securityType, this.logger, this.timeout);
/// Set current transfer type of socket
///
/// Supported types are: [TransferType.auto], [TransferType.ascii], [TransferType.binary],
TransferType get transferType => _transferType;
/// Read the FTP Server response from the Stream
///
/// Blocks until data is received!
Future<FTPReply> readResponse() async {
StringBuffer res = StringBuffer();
await Future.doWhile(() async {
bool dataReceivedSuccessfully = false;
//this is used to read all data for specific command line
while (_socket.available() > 0) {
res.write(String.fromCharCodes(_socket.read()!).trim());
dataReceivedSuccessfully = true;
}
if (dataReceivedSuccessfully) return false;
await Future.delayed(Duration(milliseconds: 300));
return true;
}).timeout(Duration(seconds: timeout), onTimeout: () {
throw FTPConnectException('Timeout reached for Receiving response !');
});
String r = res.toString();
if (r.startsWith("\n")) r = r.replaceFirst("\n", "");
if (r.length < 3) throw FTPConnectException("Illegal Reply Exception", r);
int? code;
List<String> lines = r.split('\n');
//get last code
String? line;
for (line in lines) {
if (line.length >= 3) code = int.tryParse(line.substring(0, 3)) ?? code;
}
//multiline response
if (line != null && line.length >= 4 && line[3] == '-')
return await readResponse();
if (code == null) throw FTPConnectException("Illegal Reply Exception", r);
FTPReply reply = FTPReply(code, r);
logger.log('< ${reply.toString()}');
return reply;
}
/// Send a command [cmd] to the FTP Server
/// if [waitResponse] the function waits for the reply, other wise return ''
Future<FTPReply> sendCommand(String cmd) {
logger.log('> $cmd');
_socket.write(Utf8Codec().encode('$cmd\r\n'));
return readResponse();
}
/// Send a command [cmd] to the FTP Server
/// if [waitResponse] the function waits for the reply, other wise return ''
void sendCommandWithoutWaitingResponse(String cmd) async {
logger.log('> $cmd');
_socket.write(Utf8Codec().encode('$cmd\r\n'));
}
/// Connect to the FTP Server and Login with [user] and [pass]
Future<bool> connect(String user, String pass, {String? account}) async {
logger.log('Connecting...');
final timeout = Duration(seconds: this.timeout);
try {
// FTPS starts secure
if (securityType == SecurityType.FTPS) {
_socket = await RawSecureSocket.connect(
host,
port,
timeout: timeout,
onBadCertificate: (certificate) => true,
);
} else {
_socket = await RawSocket.connect(
host,
port,
timeout: timeout,
);
}
} catch (e) {
throw FTPConnectException(
'Could not connect to $host ($port)', e.toString());
}
logger.log('Connection established, waiting for welcome message...');
await readResponse();
// FTPES needs to be upgraded prior to getting a welcome
if (securityType == SecurityType.FTPES) {
FTPReply lResp = await sendCommand('AUTH TLS');
if (!lResp.isSuccessCode()) {
lResp = await sendCommand('AUTH SSL');
if (!lResp.isSuccessCode()) {
throw FTPConnectException(
'FTPES cannot be applied: the server refused both AUTH TLS and AUTH SSL commands',
lResp.message);
}
}
_socket = await RawSecureSocket.secure(_socket,
onBadCertificate: (certificate) => true);
}
if ([SecurityType.FTPES, SecurityType.FTPS].contains(securityType)) {
await sendCommand('PBSZ 0');
await sendCommand('PROT P');
}
// Send Username
FTPReply lResp = await sendCommand('USER $user');
//password required
if (lResp.code == 331) {
lResp = await sendCommand('PASS $pass');
if (lResp.code == 332) {
if (account == null) throw FTPConnectException('Account required');
lResp = await sendCommand('ACCT $account');
if (!lResp.isSuccessCode()) {
throw FTPConnectException('Wrong Account', lResp.message);
}
} else if (!lResp.isSuccessCode()) {
throw FTPConnectException('Wrong Username/password', lResp.message);
}
//account required
} else if (lResp.code == 332) {
if (account == null) throw FTPConnectException('Account required');
lResp = await sendCommand('ACCT $account');
if (!lResp.isSuccessCode()) {
throw FTPConnectException('Wrong Account', lResp.message);
}
} else if (!lResp.isSuccessCode()) {
throw FTPConnectException('Wrong username $user', lResp.message);
}
logger.log('Connected!');
return true;
}
Future<FTPReply> openDataTransferChannel() async {
FTPReply res = FTPReply(200, "");
if (transferMode == TransferMode.active) {
//todo later
} else {
res = await sendCommand(supportIPV6 ? 'EPSV' : 'PASV');
if (!res.isSuccessCode()) {
throw FTPConnectException('Could not start Passive Mode', res.message);
}
}
return res;
}
/// Set the Transfer mode on [socket] to [mode]
Future<void> setTransferType(TransferType pTransferType) async {
//if we already in the same transfer type we do nothing
if (_transferType == pTransferType) return;
switch (pTransferType) {
case TransferType.auto:
// Set to ASCII mode
await sendCommand('TYPE A');
break;
case TransferType.ascii:
// Set to ASCII mode
await sendCommand('TYPE A');
break;
case TransferType.binary:
// Set to BINARY mode
await sendCommand('TYPE I');
break;
default:
break;
}
_transferType = pTransferType;
}
// Disconnect from the FTP Server
Future<bool> disconnect() async {
logger.log('Disconnecting...');
try {
await sendCommand('QUIT');
} catch (ignored) {
// Ignore
} finally {
await _socket.close();
_socket.shutdown(SocketDirection.both);
}
logger.log('Disconnected!');
return true;
}
}

307
lib/core/utils/ftpconnect/src/ftpconnect_base.dart

@ -0,0 +1,307 @@
import 'dart:io';
import '../ftpconnect.dart';
import 'package:path/path.dart';
import 'commands/directory.dart';
import 'commands/file.dart';
import 'ftp_reply.dart';
import 'ftp_socket.dart';
import 'utils.dart';
class FTPConnect {
final String _user;
final String _pass;
late FTPSocket _socket;
/// Create a FTP Client instance
///
/// [host]: Hostname or IP Address
/// [port]: Port number (Defaults to 21 for FTP and FTPES, 990 for FTPS)
/// [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,
String user = 'anonymous',
String pass = '',
bool showLog = false,
SecurityType securityType = SecurityType.FTP,
Logger? logger,
int timeout = 30,
}) : _user = user,
_pass = pass {
port ??= securityType == SecurityType.FTPS ? 990 : 21;
_socket = FTPSocket(
host,
port,
securityType,
logger != null ? logger : Logger(isEnabled: showLog),
timeout,
);
}
set transferMode(TransferMode pTransferMode) {
_socket.transferMode = pTransferMode;
}
set listCommand(ListCommand pListCommand) {
_socket.listCommand = pListCommand;
}
set supportIPV6(bool pSupportIPV6) => _socket.supportIPV6 = pSupportIPV6;
/// Set current transfer type of connection
///
/// Supported types are: [TransferType.auto], [TransferType.ascii], [TransferType.binary],
Future<void> setTransferType(TransferType pTransferType) async {
if (_socket.transferType == pTransferType) return;
await _socket.setTransferType(pTransferType);
}
/// 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();
Future<FTPReply> sendCustomCommand(String pCmd) => _socket.sendCommand(pCmd);
/// Upload the File [fFile] to the current directory
Future<bool> uploadFile(
File fFile, {
String sRemoteName = '',
FileProgress? onProgress,
}) {
return FTPFile(_socket).upload(
fFile,
remoteName: sRemoteName,
onProgress: onProgress,
);
}
/// Download the Remote File [sRemoteName] to the local File [fFile]
Future<bool> downloadFile(
String? sRemoteName,
File fFile, {
FileProgress? onProgress,
}) {
return FTPFile(_socket)
.download(sRemoteName, fFile, onProgress: onProgress);
}
/// 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) async {
String currentDir = await this.currentDirectory();
if (!await this.changeDirectory(sDirectory)) {
throw FTPConnectException("Couldn't change directory to $sDirectory");
}
List<FTPEntry> dirContent = await this.listDirectoryContent();
await Future.forEach(dirContent, (FTPEntry entry) async {
if (entry.type == FTPEntryType.FILE) {
if (!await deleteFile(entry.name)) {
throw FTPConnectException("Couldn't delete file ${entry.name}");
}
} else {
if (!await deleteDirectory(entry.name)) {
throw FTPConnectException("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() {
return FTPDirectory(_socket).directoryContent();
}
/// 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).directoryContentNames();
}
/// 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 Utils.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 Utils.retryAction(() => downloadFileRetry(), pRetryCount);
}
/// Download the Remote Directory [pRemoteDir] to the local File [pLocalDir]
/// [pRetryCount] number of attempts
Future<bool> downloadDirectory(String pRemoteDir, Directory pLocalDir,
{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 FTPConnectException('Cannot download directory',
'$pRemoteDir not found or inaccessible !');
}
List<FTPEntry> dirContent = await this.listDirectoryContent();
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 Utils.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;
}
}
///Note that [LIST] and [MLSD] return content detailed
///BUT [NLST] return only dir/file names inside the given directory
enum ListCommand { NLST, LIST, MLSD }
enum TransferType { auto, ascii, binary }
enum TransferMode { active, passive }
enum SecurityType { FTP, FTPS, FTPES }
extension CommandListTypeEnum on ListCommand {
String get describeEnum =>
this.toString().substring(this.toString().indexOf('.') + 1);
}

13
lib/core/utils/ftpconnect/src/logger.dart

@ -0,0 +1,13 @@
class Logger {
final bool isEnabled;
Logger({this.isEnabled = false});
void log(String pMessage) {
if (isEnabled) _printLog(pMessage);
}
void _printLog(String pMessage) {
print('[${DateTime.now().toString()}] $pMessage');
}
}

57
lib/core/utils/ftpconnect/src/utils.dart

@ -0,0 +1,57 @@
import 'dart:async';
class Utils {
Utils._();
static int parsePort(String response, bool isIPV6) {
return isIPV6 ? parsePortEPSV(response) : parsePortPASV(response);
}
/// Parse the Passive Mode Port from the Servers [sResponse]
/// port format (|||xxxxx|)
static int parsePortEPSV(String sResponse) {
int iParOpen = sResponse.indexOf('(');
int iParClose = sResponse.indexOf(')');
if (iParClose > -1 && iParOpen > -1) {
sResponse = sResponse.substring(iParOpen + 4, iParClose - 1);
}
return int.parse(sResponse);
}
/// Parse the Passive Mode Port from the Servers [sResponse]
/// format 227 Entering Passive Mode (192,168,8,36,8,75).
static int parsePortPASV(String sResponse) {
int iParOpen = sResponse.indexOf('(');
int iParClose = sResponse.indexOf(')');
String sParameters = sResponse.substring(iParOpen + 1, iParClose);
List<String> lstParameters = sParameters.split(',');
int iPort1 = int.parse(lstParameters[lstParameters.length - 2]);
int iPort2 = int.parse(lstParameters[lstParameters.length - 1]);
return (iPort1 * 256) + iPort2;
}
///retry a function [retryCount] times, until exceed [retryCount] or execute the function successfully
///Return true if the future executed successfully , false other wises
static Future<bool> retryAction(FutureOr<bool> action(), retryCount) async {
int lAttempts = 1;
bool result = true;
await Future.doWhile(() async {
try {
result = await action();
//if there is no exception we exit the loop (return false to exit)
return false;
} catch (e) {
if (lAttempts++ >= retryCount) {
throw e;
}
}
//return true to loop again
return true;
});
return result;
}
}
Loading…
Cancel
Save