最近在项目中遇到使用到FTP上传和下载资源,特此记录一下实现的方式。Android实现FTP的功能主要用到了Apache的Commons Net库,将Commons Net的jar包下载引入到项目中即可。
基本使用流程图如下:
FTP协议和HTTP协议有所不同,使用FTP进行下载时,你需要进行登录操作。如果服务器没设置登录功能可忽略登录操作。
private fun ftpConnect(
host: String,
port: Int,
username: String?,
password: String?,
enterLocalPassiveMode:Boolean = true
): FTPClient {
val ftpClient = FTPClient()
try {
//设置超时时间以毫秒为单位使用时,从数据连接读。
ftpClient.defaultTimeout = 10000
ftpClient.connectTimeout = 10000
ftpClient.setDataTimeout(10000)
Log.d(TAG, "connecting to the ftp server $host:$port")
//连接到FTP服务器
ftpClient.connect(host, port)
ftpClient.login(username, password)
//是否开启被动模式
if (enterLocalPassiveMode) {
ftpClient.isRemoteVerificationEnabled = false
ftpClient.enterLocalPassiveMode()
}
//请求使用UTF-8编码
ftpClient.controlEncoding = "utf-8"
val reply: Int = ftpClient.replyCode
if (!FTPReply.isPositiveCompletion(reply)) {
ftpClient.disconnect()
Log.e(TAG, "无法连接到ftp服务器,错误码为:$reply")
} else {
Log.d(TAG, "连接到ftp服务器")
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "Error: could not connect to host $host")
}
return ftpClient
}
注意:由于FTP服务器默认的编码是ISO-8859-1,因此,客户端在获取文件信息时需要请求服务器使用UTF-8编码(如果服务器支持的话),如果服务器不支持开启UTF-8编码,那么客户端在请求remotePath路径、获取文件名时,都需要对路径进行编码转换处理。
2.2 断开连接
private fun ftpDisconnect(ftpClient: FTPClient?) {
// 判断空指针
if (ftpClient == null) {
return
}
// 断开ftp服务器连接
try {
ftpClient.logout()
ftpClient.disconnect()
Log.d(TAG, "断开连接")
} catch (e: Exception) {
Log.e(TAG, "Error occurred while disconnecting from ftp server.")
}
}
注意:在下载或上传资源时首先要连接服务器,在完成操作后要记得断开连接。
2.3 上传文件
fun uploadFile(
host: String,
port: Int,
username: String?,
password: String?,
ftpSavePath: String,
ftpSaveFileName: String,
inputStream: InputStream
): Boolean {
val ftpClient = ftpConnect(host, port, username, password)
var flag = false
try {
val pathName = if (ftpSavePath.endsWith("/"))
"$ftpSavePath$ftpSaveFileName"
else
"$ftpSavePath/$ftpSaveFileName"
Log.i(
TAG, """
上传文件的路径 :$pathName
开始上传文件...
""".trimIndent()
)
//设置文件类型
ftpClient.setFileType(FTP.BINARY_FILE_TYPE)
flag = ftpClient.storeFile(String(pathName.toByteArray(), Charsets.ISO_8859_1),inputStream)
inputStream.close()
} catch (e: Exception) {
Log.e(TAG, e.toString())
} finally {
Log.i(TAG, "$ftpSaveFileName 文件上传 :" + if (flag) "成功" else "失败 ")
try {
inputStream.close()
} catch (e: IOException) {
Log.e(TAG, e.toString())
} finally {
ftpDisconnect(ftpClient)
}
}
return flag
}
注意:如果服务器不支持UTF-8编码,则要按上面的String(pathName.toByteArray(), Charsets.ISO_8859_1)的写法,进行编码转换处理,不然中文会乱码。如果支持UTF-8编码,则直接使用中文字符。
2.4 下载文件
fun downloadFile(
host: String,
port: Int,
username: String?,
password: String?,
pathName: String,
localPath: String
): Boolean {
val ftpClient = ftpConnect(host, port, username, password)
var flag = false
var outputStream: OutputStream? = null
try {
Log.i(
TAG, """
下载文件的路径 :$pathName
开始下载文件...
""".trimIndent()
)
ftpClient.setFileType(FTP.BINARY_FILE_TYPE)
createLocalDirectory(localPath)
outputStream = FileOutputStream(File(localPath))
// 下载文件
flag = ftpClient.retrieveFile(
String(pathName.toByteArray(), Charsets.ISO_8859_1),
outputStream
)
outputStream.close()
} catch (e: Exception) {
Log.e(TAG, e.toString())
} finally {
Log.i(TAG, "$pathName 文件 :" + if (flag) "成功" else "失败 ")
if (null != outputStream) {
try {
outputStream.close()
} catch (e: IOException) {
Log.e(TAG, e.toString())
} finally {
ftpDisconnect(ftpClient)
}
}
}
return flag
}
//创建本地多层目录文件,如果已存在该文件,则不创建,如果无,则创建
@Throws(IOException::class)
private fun createLocalDirectory(localPath: String) {
val path = localPath.substringBeforeLast("/")
val file = File(path)
if (!file.exists()) {
file.mkdirs()
}
}
三、其他操作
为了安全,服务器可能只给了上传和下载的权限,也有可能给了其他操作权限,如删除文件、新建目录等。
3.1 创建目录
@Throws(IOException::class)
fun createDirectory(
host: String,
port: Int,
username: String?,
password: String?,
remote: String
): Boolean {
val ftpClient = ftpConnect(host, port, username, password)
var flag = true
val directory = remote.substring(0, remote.lastIndexOf("/") + 1)
if (!directory.equals("/", ignoreCase = true) &&
!ftpClient.changeWorkingDirectory(String(directory.toByteArray(), Charsets.ISO_8859_1))
) {
// 如果远程目录不存在,则递归创建远程服务器目录
var start = 0
var end = 0
start = if (directory.startsWith("/")) {
1
} else {
0
}
end = directory.indexOf("/", start)
while (true) {
val subDirectory = String(
remote.substring(start, end).toByteArray(), Charsets.ISO_8859_1
)
if (!ftpClient.changeWorkingDirectory(subDirectory)) {
if (ftpClient.makeDirectory(subDirectory)) {
ftpClient.changeWorkingDirectory(subDirectory)
} else {
flag = false
}
}
start = end + 1
end = directory.indexOf("/", start)
// 检查所有目录是否创建完毕
if (end <= start) {
break
}
}
}
Log.i(TAG, "$remote 文件创建 :" + if (flag) "成功" else "失败 ")
ftpDisconnect(ftpClient)
return flag
}
3.2 删除文件
fun deleteFile(
host: String,
port: Int,
username: String?,
password: String?,
serverPath: String
): Boolean {
val ftpClient = ftpConnect(host, port, username, password)
val flag = deleteFile(ftpClient, serverPath)
Log.i(TAG, "$serverPath 文件删除 :" + if (flag) "成功" else "失败 ")
ftpDisconnect(ftpClient)
return flag
}
private fun deleteFile(ftpClient: FTPClient, pathName: String): Boolean {
try {
val files = ftpClient.listFiles(pathName)
if (files.isNotEmpty()) {
for (file in files) {
if (file.isDirectory) {
deleteFile(ftpClient, pathName + "/" + file.name)
// 切换到父目录,不然删不掉文件夹
ftpClient.changeWorkingDirectory(
pathName.substring(
0,
pathName.lastIndexOf("/")
)
)
ftpClient.removeDirectory(pathName)
} else {
if (!ftpClient.deleteFile(pathName + "/" + file.name)) {
return false
}
}
}
}
// 切换到父目录,不然删不掉文件夹
ftpClient.changeWorkingDirectory(pathName.substring(0, pathName.lastIndexOf("/")))
ftpClient.removeDirectory(pathName)
} catch (e: IOException) {
e.printStackTrace()
}
return true
}



