10 changed files with 1192 additions and 0 deletions
@ -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'; |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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)'; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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'); |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue