组件化功能基本完成

This commit is contained in:
laoyuyu
2019-09-29 20:07:21 +08:00
parent 7ee1347ab4
commit 2a92321785
337 changed files with 5186 additions and 3250 deletions

View File

@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.ariaftpcomponent" />

View File

@@ -0,0 +1,421 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp;
import android.net.TrafficStats;
import android.os.Process;
import android.text.TextUtils;
import aria.apache.commons.net.ftp.FTP;
import aria.apache.commons.net.ftp.FTPClient;
import aria.apache.commons.net.ftp.FTPClientConfig;
import aria.apache.commons.net.ftp.FTPFile;
import aria.apache.commons.net.ftp.FTPReply;
import aria.apache.commons.net.ftp.FTPSClient;
import com.arialyy.aria.core.AriaConfig;
import com.arialyy.aria.core.FtpUrlEntity;
import com.arialyy.aria.core.common.AbsEntity;
import com.arialyy.aria.core.wrapper.AbsTaskWrapper;
import com.arialyy.aria.core.inf.OnFileInfoCallback;
import com.arialyy.aria.core.upload.UploadEntity;
import com.arialyy.aria.exception.AriaIOException;
import com.arialyy.aria.exception.BaseException;
import com.arialyy.aria.exception.FileNotFoundException;
import com.arialyy.aria.exception.TaskException;
import com.arialyy.aria.util.ALog;
import com.arialyy.aria.util.CommonUtil;
import com.arialyy.aria.util.Regular;
import com.arialyy.aria.util.SSLContextUtil;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.SSLContext;
/**
* Created by Aria.Lao on 2017/7/25. 获取ftp文件夹信息
*/
public abstract class AbsFtpInfoThread<ENTITY extends AbsEntity, TASK_WRAPPER extends AbsTaskWrapper<ENTITY>>
implements Runnable {
private final String TAG = "AbsFtpInfoThread";
protected ENTITY mEntity;
protected TASK_WRAPPER mTaskWrapper;
protected FtpTaskOption mTaskOption;
private int mConnectTimeOut;
protected OnFileInfoCallback mCallback;
protected long mSize = 0;
protected String charSet = "UTF-8";
private boolean isUpload = false;
public AbsFtpInfoThread(TASK_WRAPPER taskWrapper, OnFileInfoCallback callback) {
mTaskWrapper = taskWrapper;
mEntity = taskWrapper.getEntity();
mTaskOption = (FtpTaskOption) taskWrapper.getTaskOption();
mConnectTimeOut = AriaConfig.getInstance().getDConfig().getConnectTimeOut();
mCallback = callback;
if (mEntity instanceof UploadEntity) {
isUpload = true;
}
}
/**
* 获取请求的远程文件路径
*
* @return 远程文件路径
*/
protected abstract String getRemotePath();
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
TrafficStats.setThreadStatsTag(UUID.randomUUID().toString().hashCode());
FTPClient client = null;
try {
client = createFtpClient();
if (client == null) {
ALog.e(TAG, String.format("任务【%s】失败", mTaskOption.getUrlEntity().url));
return;
}
String remotePath = CommonUtil.convertFtpChar(charSet, getRemotePath());
FTPFile[] files = client.listFiles(remotePath);
boolean isExist = files.length != 0;
if (!isExist && !isUpload) {
int i = remotePath.lastIndexOf(File.separator);
FTPFile[] files1;
if (i == -1) {
files1 = client.listFiles();
} else {
files1 = client.listFiles(remotePath.substring(0, i + 1));
}
if (files1.length > 0) {
ALog.i(TAG,
String.format("路径【%s】下的文件列表 ===================================", getRemotePath()));
for (FTPFile file : files1) {
ALog.d(TAG, file.toString());
}
ALog.i(TAG,
"================================= --end-- ===================================");
} else {
ALog.w(TAG, String.format("获取文件列表失败msg%s", client.getReplyString()));
}
closeClient(client);
failDownload(new FileNotFoundException(TAG,
String.format("文件不存在url: %s, remotePath%s", mTaskOption.getUrlEntity().url,
remotePath)), false);
return;
}
// 处理拦截功能
if (!onInterceptor(client, files)) {
closeClient(client);
ALog.d(TAG, "拦截器处理完成任务,任务将不再执行");
return;
}
//为了防止编码错乱,需要使用原始字符串
mSize = getFileSize(files, client, getRemotePath());
int reply = client.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
if (isUpload) {
//服务器上没有该文件路径,表示该任务为新的上传任务
mTaskWrapper.setNewTask(true);
} else {
closeClient(client);
failDownload(new AriaIOException(TAG,
String.format("获取文件信息错误url: %s, errorCode%s, errorMsg%s",
mTaskOption.getUrlEntity().url, reply, client.getReplyString())), true);
return;
}
}
mTaskWrapper.setCode(reply);
if (mSize != 0 && !isUpload) {
mEntity.setFileSize(mSize);
}
onPreComplete(reply);
mEntity.update();
} catch (IOException e) {
failDownload(new AriaIOException(TAG,
String.format("FTP错误信息code%smsg%s", client.getReplyCode(), client.getReplyString()),
e), true);
} finally {
closeClient(client);
}
}
/**
* 处理拦截
*
* @param ftpFiles remotePath路径下的所有文件
* @return {@code false} 拦截器处理完成任务,任务将不再执行,{@code true} 拦截器处理任务完成任务,任务继续执行
*/
protected boolean onInterceptor(FTPClient client, FTPFile[] ftpFiles) {
return true;
}
/**
* 检查文件是否存在
*
* @return {@code true}存在
*/
private boolean checkFileExist(FTPFile[] ftpFiles, String fileName) {
for (FTPFile ff : ftpFiles) {
if (ff.getName().equals(fileName)) {
return true;
}
}
return false;
}
protected void onPreComplete(int code) {
}
/**
* 创建FTP客户端
*/
private FTPClient createFtpClient() {
FTPClient client = null;
final FtpUrlEntity urlEntity = mTaskOption.getUrlEntity();
try {
Pattern p = Pattern.compile(Regular.REG_IP_V4);
Matcher m = p.matcher(urlEntity.hostName);
if (m.find() && m.groupCount() > 0) {
client = newInstanceClient(urlEntity);
client.setConnectTimeout(mConnectTimeOut); // 连接10s超时
InetAddress ip = InetAddress.getByName(urlEntity.hostName);
client = connect(client, new InetAddress[] { ip }, 0, Integer.parseInt(urlEntity.port));
mTaskOption.getUrlEntity().validAddr = ip;
} else {
DNSQueryThread dnsThread = new DNSQueryThread(urlEntity.hostName);
dnsThread.start();
dnsThread.join(mConnectTimeOut);
InetAddress[] ips = dnsThread.getIps();
client = connect(newInstanceClient(urlEntity), ips, 0, Integer.parseInt(urlEntity.port));
}
if (client == null) {
failDownload(new AriaIOException(TAG,
String.format("链接失败, url: %s", mTaskOption.getUrlEntity().url)), false);
return null;
}
boolean loginSuccess = true;
if (urlEntity.needLogin) {
try {
if (TextUtils.isEmpty(urlEntity.account)) {
loginSuccess = client.login(urlEntity.user, urlEntity.password);
} else {
loginSuccess = client.login(urlEntity.user, urlEntity.password, urlEntity.account);
}
} catch (IOException e) {
ALog.e(TAG,
new TaskException(TAG, String.format("登录失败,错误码为:%s msg%s", client.getReplyCode(),
client.getReplyString()), e));
return null;
}
}
if (!loginSuccess) {
failDownload(
new TaskException(TAG, String.format("登录失败,错误码为:%s msg%s", client.getReplyCode(),
client.getReplyString())),
false);
client.disconnect();
return null;
}
int reply = client.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
client.disconnect();
failDownload(new AriaIOException(TAG,
String.format("无法连接到ftp服务器filePath: %s, url: %s, errorCode: %s, errorMsg%s",
mEntity.getKey(), mTaskOption.getUrlEntity().url, reply,
client.getReplyString())),
true);
return null;
}
// 开启服务器对UTF-8的支持如果服务器支持就用UTF-8编码
charSet = "UTF-8";
reply = client.sendCommand("OPTS UTF8", "ON");
if (reply != FTPReply.COMMAND_IS_SUPERFLUOUS) {
ALog.i(TAG, "D_FTP 服务器不支持开启UTF8编码尝试使用Aria手动设置的编码");
if (!TextUtils.isEmpty(mTaskOption.getCharSet())) {
charSet = mTaskOption.getCharSet();
}
}
client.setControlEncoding(charSet);
client.setDataTimeout(10 * 1000);
client.enterLocalPassiveMode();
client.setFileType(FTP.BINARY_FILE_TYPE);
} catch (IOException e) {
closeClient(client);
e.printStackTrace();
} catch (InterruptedException e) {
closeClient(client);
e.printStackTrace();
}
return client;
}
protected void closeClient(FTPClient client) {
try {
if (client != null && client.isConnected()) {
client.logout();
client.disconnect();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 创建FTP/FTPS客户端
*/
private FTPClient newInstanceClient(FtpUrlEntity urlEntity) {
FTPClient temp;
if (urlEntity.isFtps) {
FTPSClient sClient;
SSLContext sslContext = SSLContextUtil.getSSLContext(urlEntity.keyAlias, urlEntity.storePath,
urlEntity.protocol);
if (sslContext == null) {
sClient = new FTPSClient(urlEntity.protocol, urlEntity.isImplicit);
} else {
sClient = new FTPSClient(true, sslContext);
}
temp = sClient;
} else {
temp = new FTPClient();
}
FTPClientConfig clientConfig;
if (mTaskOption.getClientConfig() != null) {
clientConfig = mTaskOption.getClientConfig();
} else {
clientConfig = new FTPClientConfig(FTPClientConfig.SYST_UNIX);
clientConfig.setServerLanguageCode("en");
}
temp.configure(clientConfig);
return temp;
}
/**
* 连接到ftp服务器
*/
private FTPClient connect(FTPClient client, InetAddress[] ips, int index, int port) {
if (ips == null || ips.length == 0) {
ALog.w(TAG, "无可用ip");
return null;
}
try {
client.setConnectTimeout(mConnectTimeOut); //需要先设置超时,这样才不会出现阻塞
client.connect(ips[index], port);
mTaskOption.getUrlEntity().validAddr = ips[index];
FtpUrlEntity urlEntity = mTaskOption.getUrlEntity();
if (urlEntity.isFtps) {
FTPSClient sClient = (FTPSClient) client;
sClient.execPBSZ(0);
sClient.execPROT("P");
}
return client;
} catch (IOException e) {
e.printStackTrace();
closeClient(client);
if (index + 1 >= ips.length) {
ALog.w(TAG, "遇到[ECONNREFUSED-连接被服务器拒绝]错误已没有其他地址链接失败如果是ftps请检查端口是否使用了ftp的端口而不是ftps的端口");
return null;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
ALog.w(TAG, "遇到[ECONNREFUSED-连接被服务器拒绝]错误,正在尝试下一个地址");
return connect(newInstanceClient(mTaskOption.getUrlEntity()), ips, index + 1, port);
}
}
/**
* 遍历FTP服务器上对应文件或文件夹大小
*
* @throws IOException 字符串编码转换错误
*/
private long getFileSize(FTPFile[] files, FTPClient client, String dirName) throws IOException {
long size = 0;
String path = dirName + "/";
for (FTPFile file : files) {
if (file.isFile()) {
size += file.getSize();
ALog.d(TAG, "isValid = " + file.isValid());
handleFile(path + file.getName(), file);
} else {
String remotePath = CommonUtil.convertFtpChar(charSet, path + file.getName());
size += getFileSize(client.listFiles(remotePath), client, path + file.getName());
}
}
return size;
}
/**
* 处理FTP文件信息
*
* @param remotePath ftp服务器文件夹路径
* @param ftpFile ftp服务器上对应的文件
*/
protected void handleFile(String remotePath, FTPFile ftpFile) {
}
protected void failDownload(BaseException e, boolean needRetry) {
if (mCallback != null) {
mCallback.onFail(mEntity, e, needRetry);
}
}
/**
* 获取可用IP的超时线程InetAddress.getByName没有超时功能需要自己处理超时
*/
private static class DNSQueryThread extends Thread {
private String hostName;
private InetAddress[] ips;
DNSQueryThread(String hostName) {
this.hostName = hostName;
}
@Override
public void run() {
try {
ips = InetAddress.getAllByName(hostName);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
synchronized InetAddress[] getIps() {
return ips;
}
}
}

View File

@@ -0,0 +1,194 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp;
import android.text.TextUtils;
import aria.apache.commons.net.ftp.FTP;
import aria.apache.commons.net.ftp.FTPClient;
import aria.apache.commons.net.ftp.FTPClientConfig;
import aria.apache.commons.net.ftp.FTPReply;
import aria.apache.commons.net.ftp.FTPSClient;
import com.arialyy.aria.core.FtpUrlEntity;
import com.arialyy.aria.core.common.SubThreadConfig;
import com.arialyy.aria.core.task.AbsThreadTaskAdapter;
import com.arialyy.aria.exception.AriaIOException;
import com.arialyy.aria.util.ALog;
import com.arialyy.aria.util.SSLContextUtil;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import javax.net.ssl.SSLContext;
/**
* @Author lyy
* @Date 2019-09-18
*/
public abstract class BaseFtpThreadTaskAdapter extends AbsThreadTaskAdapter {
protected FtpTaskOption mTaskOption;
protected String charSet;
protected BaseFtpThreadTaskAdapter(SubThreadConfig config) {
super(config);
}
protected void closeClient(FTPClient client) {
try {
if (client != null && client.isConnected()) {
client.logout();
client.disconnect();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 构建FTP客户端
*/
protected FTPClient createClient() {
FTPClient client = null;
final FtpUrlEntity urlEntity = mTaskOption.getUrlEntity();
if (urlEntity.validAddr == null) {
try {
InetAddress[] ips = InetAddress.getAllByName(urlEntity.hostName);
client = connect(newInstanceClient(urlEntity), ips, 0, Integer.parseInt(urlEntity.port));
if (client == null) {
return null;
}
} catch (UnknownHostException e) {
e.printStackTrace();
}
} else {
client = newInstanceClient(urlEntity);
try {
client.connect(urlEntity.validAddr, Integer.parseInt(urlEntity.port));
} catch (java.io.IOException e) {
ALog.e(TAG, ALog.getExceptionString(e));
return null;
}
}
if (client == null) {
return null;
}
try {
if (urlEntity.isFtps) {
FTPSClient sClient = (FTPSClient) client;
sClient.execPBSZ(0);
sClient.execPROT("P");
}
if (urlEntity.needLogin) {
if (TextUtils.isEmpty(urlEntity.account)) {
client.login(urlEntity.user, urlEntity.password);
} else {
client.login(urlEntity.user, urlEntity.password, urlEntity.account);
}
}
int reply = client.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
client.disconnect();
fail(new AriaIOException(TAG,
String.format("无法连接到ftp服务器错误码为%smsg:%s", reply, client.getReplyString())), false);
return null;
}
// 开启服务器对UTF-8的支持如果服务器支持就用UTF-8编码
charSet = "UTF-8";
if (reply != FTPReply.COMMAND_IS_SUPERFLUOUS) {
if (!TextUtils.isEmpty(mTaskOption.getCharSet())) {
charSet = mTaskOption.getCharSet();
}
}
client.setControlEncoding(charSet);
client.setDataTimeout(getTaskConfig().getIOTimeOut());
client.setConnectTimeout(getTaskConfig().getConnectTimeOut());
client.enterLocalPassiveMode();
client.setFileType(FTP.BINARY_FILE_TYPE);
client.setControlKeepAliveTimeout(5000);
} catch (IOException e) {
closeClient(client);
e.printStackTrace();
}
return client;
}
/**
* 创建FTP/FTPS客户端
*/
private FTPClient newInstanceClient(FtpUrlEntity urlEntity) {
FTPClient temp;
if (urlEntity.isFtps) {
FTPSClient sClient;
SSLContext sslContext = SSLContextUtil.getSSLContext(urlEntity.keyAlias, urlEntity.storePath,
urlEntity.protocol);
if (sslContext == null) {
sClient = new FTPSClient(urlEntity.protocol, urlEntity.isImplicit);
} else {
sClient = new FTPSClient(true, sslContext);
}
temp = sClient;
} else {
temp = new FTPClient();
}
FTPClientConfig clientConfig;
if (mTaskOption.getClientConfig() != null) {
clientConfig = mTaskOption.getClientConfig();
} else {
clientConfig = new FTPClientConfig(FTPClientConfig.SYST_UNIX);
clientConfig.setServerLanguageCode("en");
}
temp.configure(clientConfig);
return temp;
}
/**
* 连接到ftp服务器
*/
private FTPClient connect(FTPClient client, InetAddress[] ips, int index, int port) {
try {
client.connect(ips[index], port);
mTaskOption.getUrlEntity().validAddr = ips[index];
return client;
} catch (java.io.IOException e) {
try {
if (client.isConnected()) {
client.disconnect();
}
} catch (java.io.IOException e1) {
e1.printStackTrace();
}
if (index + 1 >= ips.length) {
ALog.w(TAG, "遇到[ECONNREFUSED-连接被服务器拒绝]错误,已没有其他地址,链接失败");
return null;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
ALog.w(TAG, "遇到[ECONNREFUSED-连接被服务器拒绝]错误,正在尝试下一个地址");
return connect(new FTPClient(), ips, index + 1, port);
}
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp;
import aria.apache.commons.net.ftp.FTPFile;
import com.arialyy.aria.core.FtpUrlEntity;
import com.arialyy.aria.core.common.CompleteInfo;
import com.arialyy.aria.core.download.DGTaskWrapper;
import com.arialyy.aria.core.download.DTaskWrapper;
import com.arialyy.aria.core.download.DownloadEntity;
import com.arialyy.aria.core.download.DownloadGroupEntity;
import com.arialyy.aria.core.inf.OnFileInfoCallback;
import com.arialyy.aria.core.wrapper.AbsTaskWrapper;
import com.arialyy.aria.exception.BaseException;
import com.arialyy.aria.util.CommonUtil;
import com.arialyy.aria.util.RecordUtil;
import java.nio.charset.Charset;
import java.util.ArrayList;
/**
* Created by Aria.Lao on 2017/7/25. 获取ftp文件夹信息
*/
public class FtpDirInfoThread extends AbsFtpInfoThread<DownloadGroupEntity, DGTaskWrapper> {
public FtpDirInfoThread(DGTaskWrapper taskEntity, OnFileInfoCallback callback) {
super(taskEntity, callback);
}
@Override protected String getRemotePath() {
return mTaskOption.getUrlEntity().remotePath;
}
@Override protected void handleFile(String remotePath, FTPFile ftpFile) {
super.handleFile(remotePath, ftpFile);
addEntity(remotePath, ftpFile);
}
@Override protected void onPreComplete(int code) {
super.onPreComplete(code);
mEntity.setFileSize(mSize);
mCallback.onComplete(mEntity.getKey(), new CompleteInfo(code, mTaskWrapper));
}
/**
* FTP文件夹的子任务实体 在这生成
*/
private void addEntity(String remotePath, FTPFile ftpFile) {
final FtpUrlEntity urlEntity = mTaskOption.getUrlEntity().clone();
DownloadEntity entity = new DownloadEntity();
entity.setUrl(
urlEntity.scheme + "://" + urlEntity.hostName + ":" + urlEntity.port + "/" + remotePath);
entity.setFilePath(mEntity.getDirPath() + "/" + remotePath);
int lastIndex = remotePath.lastIndexOf("/");
String fileName = lastIndex < 0 ? CommonUtil.keyToHashKey(remotePath)
: remotePath.substring(lastIndex + 1);
entity.setFileName(
new String(fileName.getBytes(), Charset.forName(mTaskOption.getCharSet())));
entity.setGroupHash(mEntity.getGroupHash());
entity.setGroupChild(true);
entity.setConvertFileSize(CommonUtil.formatFileSize(ftpFile.getSize()));
entity.setFileSize(ftpFile.getSize());
entity.insert();
DTaskWrapper subWrapper = new DTaskWrapper(entity);
subWrapper.setGroupTask(true);
subWrapper.setGroupHash(mEntity.getGroupHash());
subWrapper.setRequestType(AbsTaskWrapper.D_FTP);
urlEntity.url = entity.getUrl();
urlEntity.remotePath = remotePath;
cloneInfo(subWrapper, urlEntity);
if (mEntity.getUrls() == null) {
mEntity.setUrls(new ArrayList<String>());
}
mEntity.getSubEntities().add(entity);
mTaskWrapper.getSubTaskWrapper().add(subWrapper);
}
private void cloneInfo(DTaskWrapper subWrapper, FtpUrlEntity urlEntity) {
FtpTaskOption subOption = (FtpTaskOption) subWrapper.getTaskOption();
subOption.setUrlEntity(urlEntity);
subOption.setCharSet(mTaskOption.getCharSet());
subOption.setProxy(mTaskOption.getProxy());
}
@Override protected void failDownload(BaseException e, boolean needRetry) {
super.failDownload(e, needRetry);
RecordUtil.delGroupTaskRecord(mTaskWrapper.getEntity(), true, true);
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp;
import com.arialyy.aria.core.TaskRecord;
import com.arialyy.aria.core.ThreadRecord;
import com.arialyy.aria.core.common.AbsRecordHandlerAdapter;
import com.arialyy.aria.core.common.RecordHelper;
import com.arialyy.aria.core.config.Configuration;
import com.arialyy.aria.core.download.DownloadEntity;
import com.arialyy.aria.core.common.AbsNormalEntity;
import com.arialyy.aria.core.wrapper.AbsTaskWrapper;
import com.arialyy.aria.core.inf.IRecordHandler;
import com.arialyy.aria.core.wrapper.ITaskWrapper;
import com.arialyy.aria.util.RecordUtil;
import java.util.ArrayList;
/**
* @Author lyy
* @Date 2019-09-19
*/
public class FtpRecordAdapter extends AbsRecordHandlerAdapter {
public FtpRecordAdapter(AbsTaskWrapper wrapper) {
super(wrapper);
}
@Override public void handlerTaskRecord(TaskRecord record) {
RecordHelper helper = new RecordHelper(getWrapper(), record);
if (record.isBlock) {
helper.handleBlockRecord();
} else if (record.threadNum == 1) {
helper.handleSingleThreadRecord();
}
}
@Override
public ThreadRecord createThreadRecord(TaskRecord record, int threadId, long startL, long endL) {
ThreadRecord tr;
tr = new ThreadRecord();
tr.taskKey = record.filePath;
tr.threadId = threadId;
tr.startLocation = startL;
tr.isComplete = false;
tr.threadType = TaskRecord.TYPE_HTTP_FTP;
//最后一个线程的结束位置即为文件的总长度
if (threadId == (record.threadNum - 1)) {
endL = getEntity().getFileSize();
}
tr.endLocation = endL;
tr.blockLen = RecordUtil.getBlockLen(getEntity().getFileSize(), threadId, record.threadNum);
return tr;
}
@Override public TaskRecord createTaskRecord(int threadNum) {
TaskRecord record = new TaskRecord();
record.fileName = getEntity().getFileName();
record.filePath = getEntity().getFilePath();
record.threadRecords = new ArrayList<>();
record.threadNum = threadNum;
int requestType = getWrapper().getRequestType();
if (requestType == ITaskWrapper.D_FTP || requestType == ITaskWrapper.D_FTP_DIR) {
record.isBlock = threadNum > 1 && Configuration.getInstance().downloadCfg.isUseBlock();
// 线程数为1或者使用了分块则认为是使用动态长度文件
record.isOpenDynamicFile = threadNum == 1 || record.isBlock;
} else {
record.isBlock = false;
}
record.taskType = TaskRecord.TYPE_HTTP_FTP;
record.isGroupRecord = getEntity().isGroupChild();
if (record.isGroupRecord) {
if (getEntity() instanceof DownloadEntity) {
record.dGroupHash = ((DownloadEntity) getEntity()).getGroupHash();
}
}
return record;
}
@Override public int initTaskThreadNum() {
int requestType = getWrapper().getRequestType();
if (requestType == ITaskWrapper.D_FTP || requestType == ITaskWrapper.D_FTP_DIR) {
int threadNum = Configuration.getInstance().downloadCfg.getThreadNum();
return getEntity().getFileSize() <= IRecordHandler.SUB_LEN
|| getEntity().isGroupChild()
|| threadNum == 1
? 1
: threadNum;
} else {
return 1;
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp;
import aria.apache.commons.net.ftp.FTPClientConfig;
import com.arialyy.aria.core.FtpUrlEntity;
import com.arialyy.aria.core.processor.FtpInterceptHandler;
import com.arialyy.aria.core.processor.IFtpUploadInterceptor;
import com.arialyy.aria.core.inf.ITaskOption;
import java.lang.ref.SoftReference;
import java.net.Proxy;
/**
* fTP任务设置的信息用户名、密码、端口等信息
*/
public class FtpTaskOption implements ITaskOption {
/**
* 账号和密码
*/
private FtpUrlEntity urlEntity;
private Proxy proxy;
/**
* 字符编码,默认为"utf-8"
*/
private String charSet = "utf-8";
/**
* 上传拦截器
*/
private SoftReference<IFtpUploadInterceptor> uploadInterceptor;
/**
* 上传到服务器文件的新文件名{@link FtpInterceptHandler#getNewFileName()}
*/
private String newFileName;
/**
* client配置信息
*/
private FTPClientConfig clientConfig;
public FTPClientConfig getClientConfig() {
return clientConfig;
}
public void setClientConfig(FTPClientConfig clientConfig) {
this.clientConfig = clientConfig;
}
public String getNewFileName() {
return newFileName;
}
public void setNewFileName(String newFileName) {
this.newFileName = newFileName;
}
public IFtpUploadInterceptor getUploadInterceptor() {
return uploadInterceptor == null ? null : uploadInterceptor.get();
}
public void setUploadInterceptor(IFtpUploadInterceptor uploadInterceptor) {
this.uploadInterceptor = new SoftReference<>(uploadInterceptor);
}
public FtpUrlEntity getUrlEntity() {
return urlEntity;
}
public void setUrlEntity(FtpUrlEntity urlEntity) {
this.urlEntity = urlEntity;
}
public void setProxy(Proxy proxy) {
this.proxy = proxy;
}
public Proxy getProxy() {
return proxy;
}
public String getCharSet() {
return charSet;
}
public void setCharSet(String charSet) {
this.charSet = charSet;
}
}

View File

@@ -0,0 +1,52 @@
package com.arialyy.aria.ftp;
import aria.apache.commons.net.ftp.FTPSClient;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.Locale;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.SSLSocket;
public class SSLSessionReuseFTPSClient extends FTPSClient {
SSLSessionReuseFTPSClient(boolean b, SSLContext context) {
super(b, context);
}
// adapted from:
// https://trac.cyberduck.io/browser/trunk/ftp/src/main/java/ch/cyberduck/core/ftp/FTPClient.java
@Override
protected void _prepareDataSocket_(final Socket socket) throws IOException {
if (socket instanceof SSLSocket) {
// Control socket is SSL
final SSLSession session = ((SSLSocket) _socket_).getSession();
if (session.isValid()) {
final SSLSessionContext context = session.getSessionContext();
try {
//final Field sessionHostPortCache = context.getClass().getDeclaredField("sessionHostPortCache");
final Field sessionHostPortCache =
context.getClass().getDeclaredField("sessionsByHostAndPort");
sessionHostPortCache.setAccessible(true);
final Object cache = sessionHostPortCache.get(context);
final Method method =
cache.getClass().getDeclaredMethod("put", Object.class, Object.class);
method.setAccessible(true);
method.invoke(cache, String.format("%s:%s", socket.getInetAddress().getHostName(),
String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT), session);
method.invoke(cache, String.format("%s:%s", socket.getInetAddress().getHostAddress(),
String.valueOf(socket.getPort())).toLowerCase(Locale.ROOT), session);
} catch (NoSuchFieldException e) {
throw new IOException(e);
} catch (Exception e) {
throw new IOException(e);
}
} else {
throw new IOException("Invalid SSL Session");
}
}
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp.download;
import aria.apache.commons.net.ftp.FTPFile;
import com.arialyy.aria.core.common.CompleteInfo;
import com.arialyy.aria.core.download.DTaskWrapper;
import com.arialyy.aria.core.download.DownloadEntity;
import com.arialyy.aria.core.inf.OnFileInfoCallback;
import com.arialyy.aria.exception.AriaIOException;
import com.arialyy.aria.ftp.AbsFtpInfoThread;
import com.arialyy.aria.util.ALog;
import com.arialyy.aria.util.FileUtil;
/**
* Created by Aria.Lao on 2017/7/25.
* 获取ftp文件信息
*/
class FtpDFileInfoThread extends AbsFtpInfoThread<DownloadEntity, DTaskWrapper> {
private final String TAG = "FtpFileInfoThread";
FtpDFileInfoThread(DTaskWrapper taskEntity, OnFileInfoCallback callback) {
super(taskEntity, callback);
}
@Override protected void handleFile(String remotePath, FTPFile ftpFile) {
super.handleFile(remotePath, ftpFile);
if (!FileUtil.checkSDMemorySpace(mEntity.getFilePath(), ftpFile.getSize())) {
mCallback.onFail(mEntity, new AriaIOException(TAG,
String.format("获取ftp文件信息失败内存空间不足, filePath: %s", mEntity.getFilePath())),
false);
}
}
@Override protected String getRemotePath() {
return mTaskOption.getUrlEntity().remotePath;
}
@Override protected void onPreComplete(int code) {
ALog.i(TAG, "FTP下载预处理完成");
super.onPreComplete(code);
if (mSize != mTaskWrapper.getEntity().getFileSize()) {
mTaskWrapper.setNewTask(true);
}
mEntity.setFileSize(mSize);
mCallback.onComplete(mEntity.getUrl(), new CompleteInfo(code, mTaskWrapper));
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp.download;
import com.arialyy.aria.core.TaskRecord;
import com.arialyy.aria.core.task.AbsNormalLoaderAdapter;
import com.arialyy.aria.core.common.RecordHandler;
import com.arialyy.aria.core.common.SubThreadConfig;
import com.arialyy.aria.core.task.ThreadTask;
import com.arialyy.aria.core.wrapper.AbsTaskWrapper;
import com.arialyy.aria.core.inf.IRecordHandler;
import com.arialyy.aria.core.task.IThreadTask;
import com.arialyy.aria.core.wrapper.ITaskWrapper;
import com.arialyy.aria.ftp.FtpRecordAdapter;
import com.arialyy.aria.util.ALog;
import java.io.File;
/**
* @Author lyy
* @Date 2019-09-19
*/
final class FtpDLoaderAdapter extends AbsNormalLoaderAdapter {
FtpDLoaderAdapter(ITaskWrapper wrapper) {
super(wrapper);
}
@Override public boolean handleNewTask(TaskRecord record, int totalThreadNum) {
if (!record.isBlock) {
if (getTempFile().exists()) {
getTempFile().delete();
}
//CommonUtil.createFile(mTempFile.getPath());
} else {
for (int i = 0; i < totalThreadNum; i++) {
File blockFile =
new File(String.format(IRecordHandler.SUB_PATH, getTempFile().getPath(), i));
if (blockFile.exists()) {
ALog.d(TAG, String.format("分块【%s】已经存在将删除该分块", i));
blockFile.delete();
}
}
}
return true;
}
@Override public IThreadTask createThreadTask(SubThreadConfig config) {
ThreadTask threadTask = new ThreadTask(config);
FtpDThreadTaskAdapter adapter = new FtpDThreadTaskAdapter(config);
threadTask.setAdapter(adapter);
return threadTask;
}
@Override public IRecordHandler recordHandler(AbsTaskWrapper wrapper) {
FtpRecordAdapter adapter = new FtpRecordAdapter(wrapper);
RecordHandler handler = new RecordHandler(wrapper);
handler.setAdapter(adapter);
return handler;
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp.download;
import com.arialyy.aria.core.common.AbsEntity;
import com.arialyy.aria.core.common.CompleteInfo;
import com.arialyy.aria.core.download.DTaskWrapper;
import com.arialyy.aria.core.inf.OnFileInfoCallback;
import com.arialyy.aria.core.listener.IDLoadListener;
import com.arialyy.aria.core.loader.AbsLoader;
import com.arialyy.aria.core.loader.AbsNormalLoaderUtil;
import com.arialyy.aria.core.loader.NormalLoader;
import com.arialyy.aria.exception.BaseException;
import com.arialyy.aria.ftp.FtpTaskOption;
/**
* @Author lyy
* @Date 2019-09-19
*/
public class FtpDLoaderUtil extends AbsNormalLoaderUtil {
public FtpDLoaderUtil(DTaskWrapper wrapper, IDLoadListener downloadListener) {
super(wrapper, downloadListener);
wrapper.generateTaskOption(FtpTaskOption.class);
}
@Override protected AbsLoader createLoader() {
NormalLoader loader = new NormalLoader(getListener(), getTaskWrapper());
loader.setAdapter(new FtpDLoaderAdapter(getTaskWrapper()));
return loader;
}
@Override protected Runnable createInfoThread() {
return new FtpDFileInfoThread((DTaskWrapper) getTaskWrapper(), new OnFileInfoCallback() {
@Override public void onComplete(String url, CompleteInfo info) {
((NormalLoader) getLoader()).updateTempFile();
getLoader().start();
}
@Override public void onFail(AbsEntity entity, BaseException e, boolean needRetry) {
fail(e, needRetry);
getLoader().closeTimer();
}
});
}
}

View File

@@ -0,0 +1,215 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp.download;
import aria.apache.commons.net.ftp.FTPClient;
import aria.apache.commons.net.ftp.FTPReply;
import com.arialyy.aria.core.common.SubThreadConfig;
import com.arialyy.aria.exception.AriaIOException;
import com.arialyy.aria.exception.TaskException;
import com.arialyy.aria.ftp.BaseFtpThreadTaskAdapter;
import com.arialyy.aria.util.ALog;
import com.arialyy.aria.util.BufferedRandomAccessFile;
import com.arialyy.aria.util.CommonUtil;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
/**
* @Author lyy
* @Date 2019-09-18
*/
final class FtpDThreadTaskAdapter extends BaseFtpThreadTaskAdapter {
FtpDThreadTaskAdapter(SubThreadConfig config) {
super(config);
}
@Override protected void handlerThreadTask() {
if (getThreadRecord().isComplete) {
handleComplete();
return;
}
FTPClient client = null;
InputStream is = null;
try {
ALog.d(TAG,
String.format("任务【%s】线程__%s__开始下载【开始位置 : %s结束位置%s】", getTaskWrapper().getKey(),
getThreadRecord().threadId, getThreadRecord().startLocation,
getThreadRecord().endLocation));
client = createClient();
if (client == null) {
fail(new TaskException(TAG, "ftp client 创建失败"), false);
return;
}
if (getThreadRecord().startLocation > 0) {
client.setRestartOffset(getThreadRecord().startLocation);
}
//发送第二次指令时,还需要再做一次判断
int reply = client.getReplyCode();
if (!FTPReply.isPositivePreliminary(reply) && reply != FTPReply.COMMAND_OK) {
fail(new AriaIOException(TAG,
String.format("获取文件信息错误,错误码为:%smsg%s", reply, client.getReplyString())), false);
client.disconnect();
return;
}
String remotePath =
CommonUtil.convertFtpChar(charSet, mTaskOption.getUrlEntity().remotePath);
ALog.i(TAG, String.format("remotePath【%s】", remotePath));
is = client.retrieveFileStream(remotePath);
reply = client.getReplyCode();
if (!FTPReply.isPositivePreliminary(reply)) {
fail(new AriaIOException(TAG,
String.format("获取流失败,错误码为:%smsg%s", reply, client.getReplyString())), true);
client.disconnect();
return;
}
if (getConfig().isOpenDynamicFile) {
readDynamicFile(is);
} else {
readNormal(is);
handleComplete();
}
} catch (IOException e) {
fail(new AriaIOException(TAG, String.format("下载失败【%s】", getConfig().url), e), true);
} catch (Exception e) {
fail(new AriaIOException(TAG, String.format("下载失败【%s】", getConfig().url), e), false);
} finally {
try {
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
closeClient(client);
}
}
/**
* 处理线程完成的情况
*/
private void handleComplete() {
if (getThreadTask().isBreak()) {
return;
}
if (!getThreadTask().checkBlock()) {
return;
}
complete();
}
/**
* 动态长度文件读取方式
*/
private void readDynamicFile(InputStream is) {
FileOutputStream fos = null;
FileChannel foc = null;
ReadableByteChannel fic = null;
try {
int len;
fos = new FileOutputStream(getConfig().tempFile, true);
foc = fos.getChannel();
fic = Channels.newChannel(is);
ByteBuffer bf = ByteBuffer.allocate(getTaskConfig().getBuffSize());
while (getThreadTask().isLive() && (len = fic.read(bf)) != -1) {
if (getThreadTask().isBreak()) {
break;
}
if (mSpeedBandUtil != null) {
mSpeedBandUtil.limitNextBytes(len);
}
if (getRangeProgress() + len >= getThreadRecord().endLocation) {
len = (int) (getThreadRecord().endLocation - getRangeProgress());
bf.flip();
fos.write(bf.array(), 0, len);
bf.compact();
progress(len);
break;
} else {
bf.flip();
foc.write(bf);
bf.compact();
progress(len);
}
}
handleComplete();
} catch (IOException e) {
fail(new AriaIOException(TAG, String.format("下载失败【%s】", getConfig().url), e), true);
} finally {
try {
if (fos != null) {
fos.close();
}
if (foc != null) {
foc.close();
}
if (fic != null) {
fic.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 多线程写文件方式
*/
private void readNormal(InputStream is) {
BufferedRandomAccessFile file = null;
try {
file =
new BufferedRandomAccessFile(getConfig().tempFile, "rwd", getTaskConfig().getBuffSize());
file.seek(getThreadRecord().startLocation);
byte[] buffer = new byte[getTaskConfig().getBuffSize()];
int len;
while (getThreadTask().isLive() && (len = is.read(buffer)) != -1) {
if (getThreadTask().isBreak()) {
break;
}
if (mSpeedBandUtil != null) {
mSpeedBandUtil.limitNextBytes(len);
}
if (getRangeProgress() + len >= getThreadRecord().endLocation) {
len = (int) (getThreadRecord().endLocation - getRangeProgress());
file.write(buffer, 0, len);
progress(len);
break;
} else {
file.write(buffer, 0, len);
progress(len);
}
}
} catch (IOException e) {
fail(new AriaIOException(TAG, String.format("下载失败【%s】", getConfig().url), e), true);
} finally {
try {
if (file != null) {
file.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp.download;
import android.net.Uri;
import android.text.TextUtils;
import com.arialyy.aria.core.FtpUrlEntity;
import com.arialyy.aria.core.common.AbsEntity;
import com.arialyy.aria.core.common.CompleteInfo;
import com.arialyy.aria.core.download.DGTaskWrapper;
import com.arialyy.aria.core.download.DTaskWrapper;
import com.arialyy.aria.core.inf.IEntity;
import com.arialyy.aria.core.inf.OnFileInfoCallback;
import com.arialyy.aria.exception.BaseException;
import com.arialyy.aria.ftp.FtpDirInfoThread;
import com.arialyy.aria.ftp.FtpTaskOption;
import com.arialyy.aria.core.group.AbsGroupUtil;
import com.arialyy.aria.core.group.AbsSubDLoadUtil;
import com.arialyy.aria.core.listener.IDGroupListener;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by Aria.Lao on 2017/7/27.
* ftp文件夹下载工具
*/
public class FtpDirDLoaderUtil extends AbsGroupUtil {
private ReentrantLock LOCK = new ReentrantLock();
private Condition condition = LOCK.newCondition();
public FtpDirDLoaderUtil(IDGroupListener listener, DGTaskWrapper wrapper) {
super(listener, wrapper);
wrapper.generateTaskOption(FtpTaskOption.class);
}
@Override
protected AbsSubDLoadUtil createSubLoader(DTaskWrapper wrapper, boolean needGetFileInfo) {
return new SubDLoaderUtil(getScheduler(), wrapper, needGetFileInfo);
}
@Override protected boolean onStart() {
super.onStart();
if (getWrapper().getEntity().getFileSize() > 1) {
startDownload(true);
} else {
FtpDirInfoThread infoThread = new FtpDirInfoThread(getWrapper(), new OnFileInfoCallback() {
@Override public void onComplete(String url, CompleteInfo info) {
if (info.code >= 200 && info.code < 300) {
startDownload(false);
}
}
@Override public void onFail(AbsEntity entity, BaseException e, boolean needRetry) {
mListener.onFail(needRetry, e);
}
});
new Thread(infoThread).start();
try {
LOCK.lock();
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOCK.unlock();
}
}
return true;
}
/**
* @param needCloneInfo 第一次下载,信息已经在{@link FtpDirInfoThread}中clone了
*/
private void startDownload(boolean needCloneInfo) {
try {
LOCK.lock();
condition.signalAll();
} finally {
LOCK.unlock();
}
initState();
for (DTaskWrapper wrapper : getWrapper().getSubTaskWrapper()) {
if (needCloneInfo) {
cloneInfo(wrapper);
}
if (wrapper.getState() != IEntity.STATE_COMPLETE) {
startSubLoader(createSubLoader(wrapper, true));
}
}
}
private void cloneInfo(DTaskWrapper subWrapper) {
FtpTaskOption option = (FtpTaskOption) getWrapper().getTaskOption();
FtpUrlEntity urlEntity = option.getUrlEntity().clone();
Uri uri = Uri.parse(subWrapper.getEntity().getUrl());
String remotePath = uri.getPath();
urlEntity.remotePath = TextUtils.isEmpty(remotePath) ? "/" : remotePath;
FtpTaskOption subOption = ((FtpTaskOption) subWrapper.getTaskOption());
subOption.setUrlEntity(urlEntity);
subOption.setCharSet(option.getCharSet());
subOption.setProxy(option.getProxy());
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp.download;
import android.os.Handler;
import com.arialyy.aria.core.download.DTaskWrapper;
import com.arialyy.aria.core.loader.NormalLoader;
import com.arialyy.aria.ftp.FtpTaskOption;
import com.arialyy.aria.core.group.AbsSubDLoadUtil;
import com.arialyy.aria.core.group.ChildDLoadListener;
/**
* @Author lyy
* @Date 2019-09-28
*/
class SubDLoaderUtil extends AbsSubDLoadUtil {
/**
* @param schedulers 调度器
* @param needGetInfo {@code true} 需要获取文件信息。{@code false} 不需要获取文件信息
*/
SubDLoaderUtil(Handler schedulers, DTaskWrapper taskWrapper, boolean needGetInfo) {
super(schedulers, taskWrapper, needGetInfo);
taskWrapper.generateTaskOption(FtpTaskOption.class);
}
@Override protected NormalLoader createLoader(ChildDLoadListener listener, DTaskWrapper wrapper) {
NormalLoader loader = new NormalLoader(listener, wrapper);
FtpDLoaderAdapter adapter = new FtpDLoaderAdapter(wrapper);
loader.setAdapter(adapter);
return loader;
}
@Override public void start() {
getDownloader().start();
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp.upload;
import androidx.annotation.NonNull;
import com.arialyy.aria.util.BufferedRandomAccessFile;
import java.io.IOException;
import java.io.InputStream;
/**
* Created by lyy on 2017/9/26.
* BufferedRandomAccessFile 转 InputStream 适配器
*/
final class FtpFISAdapter extends InputStream {
private BufferedRandomAccessFile mIs;
private ProgressCallback mCallback;
private int count;
interface ProgressCallback {
void onProgressCallback(byte[] buffer, int byteOffset, int byteCount) throws IOException;
}
FtpFISAdapter(@NonNull BufferedRandomAccessFile is, @NonNull ProgressCallback callback) {
mIs = is;
mCallback = callback;
}
FtpFISAdapter(@NonNull BufferedRandomAccessFile is) {
mIs = is;
}
@Override public void close() throws IOException {
mIs.close();
}
@Override public int read() throws IOException {
return mIs.read();
}
@Override public int read(@NonNull byte[] buffer) throws IOException {
count = mIs.read(buffer);
if (mCallback != null) {
mCallback.onProgressCallback(buffer, 0, count);
}
return count;
}
@Override public int read(@NonNull byte[] buffer, int byteOffset, int byteCount)
throws IOException {
count = mIs.read(buffer, byteOffset, byteCount);
if (mCallback != null) {
mCallback.onProgressCallback(buffer, byteOffset, byteCount);
}
return count;
}
@Override public long skip(long byteCount) throws IOException {
return mIs.skipBytes((int) byteCount);
}
}

View File

@@ -0,0 +1,164 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp.upload;
import android.text.TextUtils;
import aria.apache.commons.net.ftp.FTPClient;
import aria.apache.commons.net.ftp.FTPFile;
import com.arialyy.aria.core.TaskRecord;
import com.arialyy.aria.core.ThreadRecord;
import com.arialyy.aria.core.common.CompleteInfo;
import com.arialyy.aria.core.inf.OnFileInfoCallback;
import com.arialyy.aria.core.upload.UTaskWrapper;
import com.arialyy.aria.core.upload.UploadEntity;
import com.arialyy.aria.ftp.AbsFtpInfoThread;
import com.arialyy.aria.core.processor.FtpInterceptHandler;
import com.arialyy.aria.core.processor.IFtpUploadInterceptor;
import com.arialyy.aria.util.ALog;
import com.arialyy.aria.util.CommonUtil;
import com.arialyy.aria.util.DbDataHelper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Aria.Lao on 2017/9/26.
* 单任务上传远程服务器文件信息
*/
class FtpUFileInfoThread extends AbsFtpInfoThread<UploadEntity, UTaskWrapper> {
private static final String TAG = "FtpUploadFileInfoThread";
static final int CODE_COMPLETE = 0xab1;
private boolean isComplete = false;
private String remotePath;
/**
* true 使用拦截器false 不使用拦截器
*/
private boolean useInterceptor = false;
FtpUFileInfoThread(UTaskWrapper taskEntity, OnFileInfoCallback callback) {
super(taskEntity, callback);
}
@Override protected String getRemotePath() {
return remotePath == null ?
mTaskOption.getUrlEntity().remotePath + "/" + mEntity.getFileName() : remotePath;
}
@Override protected boolean onInterceptor(FTPClient client, FTPFile[] ftpFiles) {
// 旧任务将不做处理,否则断点续传上传将失效
if (!mTaskWrapper.isNewTask()) {
return true;
}
try {
IFtpUploadInterceptor interceptor = mTaskOption.getUploadInterceptor();
if (interceptor != null) {
useInterceptor = true;
List<String> files = new ArrayList<>();
for (FTPFile ftpFile : ftpFiles) {
if (ftpFile.isDirectory()) {
continue;
}
files.add(ftpFile.getName());
}
FtpInterceptHandler interceptHandler = interceptor.onIntercept(mEntity, files);
/*
处理远端有同名文件的情况
*/
if (files.contains(mEntity.getFileName())) {
if (interceptHandler.isCoverServerFile()) {
ALog.i(TAG, String.format("远端已拥有同名文件,将覆盖该文件,文件名:%s", mEntity.getFileName()));
boolean b = client.deleteFile(CommonUtil.convertFtpChar(charSet, getRemotePath()));
ALog.d(TAG,
String.format("删除文件%scode: %s msg: %s", b ? "成功" : "失败", client.getReplyCode(),
client.getReplyString()));
} else if (!TextUtils.isEmpty(interceptHandler.getNewFileName())) {
ALog.i(TAG, String.format("远端已拥有同名文件将修改remotePath原文件名%s新文件名%s",
mEntity.getFileName(), interceptHandler.getNewFileName()));
remotePath = mTaskOption.getUrlEntity().remotePath
+ "/"
+ interceptHandler.getNewFileName();
mTaskOption.setNewFileName(interceptHandler.getNewFileName());
closeClient(client);
run();
return false;
}
}
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 如果服务器的文件长度和本地上传文件的文件长度一致,则任务任务已完成。
* 否则重新修改保存的停止位置这是因为outputStream是读不到服务器是否成功写入的。
* 而threadTask的保存的停止位置是File的InputStream的所有就会导致两端停止位置不一致
*
* @param remotePath ftp服务器文件夹路径
* @param ftpFile ftp服务器上对应的文件
*/
@Override protected void handleFile(String remotePath, FTPFile ftpFile) {
super.handleFile(remotePath, ftpFile);
if (ftpFile != null && !useInterceptor) {
//远程文件已完成
if (ftpFile.getSize() == mEntity.getFileSize()) {
isComplete = true;
ALog.d(TAG, "FTP服务器上已存在该文件【" + ftpFile.getName() + "");
} else {
ALog.w(TAG, "FTP服务器已存在未完成的文件【"
+ ftpFile.getName()
+ "size: "
+ ftpFile.getSize()
+ ""
+ "尝试从位置:"
+ (ftpFile.getSize() - 1)
+ "开始上传");
mTaskWrapper.setNewTask(false);
// 修改记录
TaskRecord record = DbDataHelper.getTaskRecord(mTaskWrapper.getKey());
if (record == null) {
record = new TaskRecord();
record.fileName = mEntity.getFileName();
record.filePath = mTaskWrapper.getKey();
record.threadRecords = new ArrayList<>();
}
ThreadRecord threadRecord;
if (record.threadRecords == null || record.threadRecords.isEmpty()) {
threadRecord = new ThreadRecord();
threadRecord.taskKey = record.filePath;
} else {
threadRecord = record.threadRecords.get(0);
}
//修改本地保存的停止地址为服务器上对应文件的大小
threadRecord.startLocation = ftpFile.getSize() - 1;
record.save();
threadRecord.save();
}
}
}
@Override protected void onPreComplete(int code) {
super.onPreComplete(code);
mCallback.onComplete(mEntity.getKey(),
new CompleteInfo(isComplete ? CODE_COMPLETE : code, mTaskWrapper));
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp.upload;
import com.arialyy.aria.core.common.AbsEntity;
import com.arialyy.aria.core.common.CompleteInfo;
import com.arialyy.aria.core.download.DTaskWrapper;
import com.arialyy.aria.core.inf.OnFileInfoCallback;
import com.arialyy.aria.core.listener.IUploadListener;
import com.arialyy.aria.core.loader.AbsLoader;
import com.arialyy.aria.core.loader.AbsNormalLoaderUtil;
import com.arialyy.aria.core.loader.NormalLoader;
import com.arialyy.aria.core.upload.UTaskWrapper;
import com.arialyy.aria.exception.BaseException;
import com.arialyy.aria.ftp.FtpTaskOption;
/**
* @Author lyy
* @Date 2019-09-19
*/
public class FtpULoaderUtil extends AbsNormalLoaderUtil {
public FtpULoaderUtil(DTaskWrapper wrapper, IUploadListener uploadListener) {
super(wrapper, uploadListener);
wrapper.generateTaskOption(FtpTaskOption.class);
}
@Override protected AbsLoader createLoader() {
NormalLoader loader = new NormalLoader(getListener(), getTaskWrapper());
loader.setAdapter(new FtpULoaferAdapter(getTaskWrapper()));
return null;
}
@Override protected Runnable createInfoThread() {
return new FtpUFileInfoThread((UTaskWrapper) getTaskWrapper(), new OnFileInfoCallback() {
@Override public void onComplete(String url, CompleteInfo info) {
if (info.code == FtpUFileInfoThread.CODE_COMPLETE) {
getListener().onComplete();
} else {
getLoader().start();
}
}
@Override public void onFail(AbsEntity entity, BaseException e, boolean needRetry) {
fail(e, needRetry);
}
});
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp.upload;
import com.arialyy.aria.core.TaskRecord;
import com.arialyy.aria.core.task.AbsNormalLoaderAdapter;
import com.arialyy.aria.core.common.RecordHandler;
import com.arialyy.aria.core.common.SubThreadConfig;
import com.arialyy.aria.core.task.ThreadTask;
import com.arialyy.aria.core.wrapper.AbsTaskWrapper;
import com.arialyy.aria.core.inf.IRecordHandler;
import com.arialyy.aria.core.task.IThreadTask;
import com.arialyy.aria.core.wrapper.ITaskWrapper;
import com.arialyy.aria.ftp.FtpRecordAdapter;
/**
* @Author lyy
* @Date 2019-09-19
*/
class FtpULoaferAdapter extends AbsNormalLoaderAdapter {
FtpULoaferAdapter(ITaskWrapper wrapper) {
super(wrapper);
}
@Override public boolean handleNewTask(TaskRecord record, int totalThreadNum) {
return true;
}
@Override public IThreadTask createThreadTask(SubThreadConfig config) {
ThreadTask threadTask = new ThreadTask(config);
FtpUThreadTaskAdapter adapter = new FtpUThreadTaskAdapter(config);
threadTask.setAdapter(adapter);
return threadTask;
}
@Override public IRecordHandler recordHandler(AbsTaskWrapper wrapper) {
FtpRecordAdapter adapter = new FtpRecordAdapter(wrapper);
RecordHandler handler = new RecordHandler(wrapper);
handler.setAdapter(adapter);
return handler;
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright (C) 2016 AriaLyy(https://github.com/AriaLyy/Aria)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.arialyy.aria.ftp.upload;
import android.text.TextUtils;
import aria.apache.commons.net.ftp.FTPClient;
import aria.apache.commons.net.ftp.FTPReply;
import aria.apache.commons.net.ftp.OnFtpInputStreamListener;
import com.arialyy.aria.core.common.SubThreadConfig;
import com.arialyy.aria.core.upload.UploadEntity;
import com.arialyy.aria.exception.AriaIOException;
import com.arialyy.aria.ftp.BaseFtpThreadTaskAdapter;
import com.arialyy.aria.util.ALog;
import com.arialyy.aria.util.BufferedRandomAccessFile;
import com.arialyy.aria.util.CommonUtil;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
/**
* Created by Aria.Lao on 2017/7/28. D_FTP 单线程上传任务需要FTP 服务器给用户打开append和write的权限
*/
class FtpUThreadTaskAdapter extends BaseFtpThreadTaskAdapter {
private final String TAG = "FtpThreadTask";
private String dir, remotePath;
FtpUThreadTaskAdapter(SubThreadConfig config) {
super(config);
}
@Override protected void handlerThreadTask() {
FTPClient client = null;
BufferedRandomAccessFile file = null;
try {
ALog.d(TAG,
String.format("任务【%s】线程__%s__开始上传【开始位置 : %s结束位置%s】", getEntity().getKey(),
getThreadRecord().threadId, getThreadRecord().startLocation,
getThreadRecord().endLocation));
client = createClient();
if (client == null) {
return;
}
initPath();
client.makeDirectory(dir);
client.changeWorkingDirectory(dir);
client.setRestartOffset(getThreadRecord().startLocation);
int reply = client.getReplyCode();
if (!FTPReply.isPositivePreliminary(reply) && reply != FTPReply.FILE_ACTION_OK) {
fail(new AriaIOException(TAG,
String.format("文件上传错误,错误码为:%s, msg%s, filePath: %s", reply,
client.getReplyString(), getEntity().getFilePath())), false);
client.disconnect();
return;
}
file =
new BufferedRandomAccessFile(getConfig().tempFile, "rwd", getTaskConfig().getBuffSize());
if (getThreadRecord().startLocation != 0) {
//file.skipBytes((int) getConfig().START_LOCATION);
file.seek(getThreadRecord().startLocation);
}
boolean complete = upload(client, file);
if (!complete || getThreadTask().isBreak()) {
return;
}
ALog.i(TAG,
String.format("任务【%s】线程__%s__上传完毕", getEntity().getKey(), getThreadRecord().threadId));
complete();
} catch (IOException e) {
fail(new AriaIOException(TAG,
String.format("上传失败filePath: %s, uploadUrl: %s", getEntity().getFilePath(),
getConfig().url)), true);
} catch (Exception e) {
fail(new AriaIOException(TAG, null, e), false);
} finally {
try {
if (file != null) {
file.close();
}
} catch (IOException e) {
e.printStackTrace();
}
closeClient(client);
}
}
private UploadEntity getEntity() {
return (UploadEntity) getTaskWrapper().getEntity();
}
private void initPath() throws UnsupportedEncodingException {
dir = CommonUtil.convertFtpChar(charSet, mTaskOption.getUrlEntity().remotePath);
String fileName =
TextUtils.isEmpty(mTaskOption.getNewFileName()) ? CommonUtil.convertFtpChar(charSet,
getEntity().getFileName())
: CommonUtil.convertFtpChar(charSet, mTaskOption.getNewFileName());
remotePath =
CommonUtil.convertFtpChar(charSet,
String.format("%s/%s", mTaskOption.getUrlEntity().remotePath, fileName));
}
/**
* 上传
*
* @return {@code true}上传成功、{@code false} 上传失败
*/
private boolean upload(final FTPClient client, final BufferedRandomAccessFile bis)
throws IOException {
try {
ALog.d(TAG, String.format("remotePath: %s", remotePath));
client.storeFile(remotePath, new FtpFISAdapter(bis), new OnFtpInputStreamListener() {
boolean isStoped = false;
@Override public void onFtpInputStream(FTPClient client, long totalBytesTransferred,
int bytesTransferred, long streamSize) {
try {
if (getThreadTask().isBreak() && !isStoped) {
isStoped = true;
client.abor();
}
if (mSpeedBandUtil != null) {
mSpeedBandUtil.limitNextBytes(bytesTransferred);
}
progress(bytesTransferred);
} catch (IOException e) {
e.printStackTrace();
}
}
});
} catch (IOException e) {
String msg = String.format("文件上传错误,错误码为:%s, msg%s, filePath: %s", client.getReplyCode(),
client.getReplyString(), getEntity().getFilePath());
if (client.isConnected()) {
client.disconnect();
}
if (e.getMessage().contains("AriaIOException caught while copying")) {
e.printStackTrace();
} else {
fail(new AriaIOException(TAG, msg, e), true);
}
return false;
}
int reply = client.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
if (reply != FTPReply.TRANSFER_ABORTED) {
fail(new AriaIOException(TAG,
String.format("文件上传错误,错误码为:%s, msg%s, filePath: %s", reply, client.getReplyString(),
getEntity().getFilePath())), false);
}
if (client.isConnected()) {
client.disconnect();
}
return false;
}
return true;
}
}

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">AriaFtpComponent</string>
</resources>