前言
时隔这么久我又回来了,最近忙里偷闲写了一个FTP上传文件的demo,用于上传公司发布的USDK服务的log文件,之前发布的USDK在客户那儿出现了不少bug,而解决这些bug需要我们的辛勤测试人员根据客户描述的现象复现修正,效率极低;那干脆就把现场log直接上传就好了呗~(不禁吐槽公司这么久了连个文件上传的管理服务器都没有,唉~);咳咳,言归正传,这个demo主要包含了三点:生成log文件/log文件的压缩/上传服务器,请看下文!
正文
生成log文件
这一趴其实没什么好讲的,正常的IO文件操作,所以就直接上代码了:代码片
/**
* 保存log到文件
*
* @param tag log的tag值
* @param finalMsg log的内容
*/
private static void writeToLog(String tag, String finalMsg) {
File logFile = new File(LOG_FILE_PATH, LOG_FILE_NAME);
File parentFile = logFile.getParentFile();
if (!parentFile.exists()) {
if (!parentFile.mkdirs()) {
return;
}
}
//delete file if over MAX_LOG_SIZE
if (logFile.exists() && logFile.length() > MAX_LOG_SIZE) {
logFile.delete();
}
if (!logFile.exists()) {
try {
if (!logFile.createNewFile()) {
return;
}
} catch (IOException e) {
e.printStackTrace();
}
}
BufferedOutputStream outputStream = null;
try {
outputStream = new BufferedOutputStream(new FileOutputStream(logFile, true));
String content = FtpCommonUtils.getTagTime(DATE_FORMAT) + " " + tag + " " + finalMsg + "\n";
outputStream.write(content.getBytes());
outputStream.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
我也没对文件进行时间的划分,单个log文件就够用,只是对log文件的大小进行了限制,这里打log的方法稍微进行了封装,代码片
public static void d(String msg) {
String tag = getLogTag();
String finalMsg = buildMsg(msg);
if (curLevel <= TAG_LEVEL_D) {
Log.d(TAG + " " + tag, finalMsg);
}
if (isWriteLogFile) {
writeToLog(tag, finalMsg);
}
}
/**
* 返回包含调用方法名的log信息
*
* @param msg 原始传入的信息
* @return 包含调用方法名的log信息
*/
private static String buildMsg(String msg) {
StackTraceElement[] element = new Throwable().fillInStackTrace().getStackTrace();
String methodName = element[2].getMethodName();
return methodName + " " + msg;
}
/**
* 获取当前打印的类名
*
* @return 当前调用的类名
*/
private static String getLogTag() {
StackTraceElement[] element = new Throwable().fillInStackTrace().getStackTrace();
String fullName = element[2].getClassName();
String simpleName = fullName;
if (fullName != null && fullName.contains(".")) {
String[] buffer = fullName.split("\\.");
simpleName = buffer[buffer.length - 1];
}
return simpleName;
}
这里直接通过方法栈找到调用封装打印接口的类和方法;使用了Throwable()类获取当前方法栈的内容,获取方法栈的方法有两种:
//method 1
Thread.currentThread().getStackTrace()
//method 2
new Throwable().getStackTrace()
不同之处在于方法一会先打印生成使用方法,具体见下(部分数据,使用方法getClassName和getMethodName打印):
--------------------------Throwable-------------------
com.topwise.compress.utils.LogUtil;getLogTag
com.topwise.compress.utils.LogUtil;d
com.topwise.compress.MainActivity;onClick
-------------------------currentThread---------------
dalvik.system.VMStack;getThreadStackTrace
java.lang.Thread;getStackTrace
com.topwise.compress.utils.LogUtil;getLogTag
com.topwise.compress.utils.LogUtil;d
com.topwise.compress.MainActivity;onClick
log文件的压缩
压缩文件使用压缩文件流,主要的类是ZipOutputStream ZipEntry,可实现压缩操作,代码如下代码片
/**
* 压缩文件方法
*
* @param srcFileName 待压缩文件(夹)路径
* @return 返回压缩后的文件路径
*/
String FileZip(String[] srcFileName) {
LogUtil.d("src file name: " + srcFileName);
//create zip file
File zipFile = getZipFile();
if (zipFile == null) {
return null;
}
//find src files
File[] srcFiles = getSrcFiles(srcFileName);
if (srcFiles == null) {
return null;
}
ZipOutputStream zipOutputStream = null;
FileInputStream fileInputStream = null;
try {
zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFile));
for (File srcFile : srcFiles) {
// 将源文件数组中的当前文件读入 FileInputStream 流中
fileInputStream = new FileInputStream(srcFile);
// 实例化 ZipEntry 对象,源文件数组中的当前文件
ZipEntry zipEntry = new ZipEntry(srcFile.getName());
zipOutputStream.putNextEntry(zipEntry);
// 该变量记录每次真正读的字节个数
int len;
// 定义每次读取的字节数组
byte[] buffer = new byte[1024];
while ((len = fileInputStream.read(buffer)) > 0) {
zipOutputStream.write(buffer, 0, len);
}
}
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
try {
if (zipOutputStream != null) {
zipOutputStream.closeEntry();
zipOutputStream.close();
}
if (fileInputStream != null) {
fileInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return zipFile.getAbsolutePath();
}
生成压缩文件使用putNextEntry方法,读取压缩文件使用getNextEntry,这个也是正常的IO流操作。
这里在查找log文件夹下我需要的log文件时,使用了文件过滤器:代码块
private class ZipFileFileFilter implements FileFilter {
@Override
public boolean accept(File pathname) {
return pathname.getName().endsWith(".log") || pathname.getName().endsWith(".jpg");
}
}
FileFilter这个接口的使用只要实现其accept方法,把你需要的过滤的文件限制写好,调用file.listFiles(new ZipFileFileFilter())即可过滤出你想要的文件;
上传文件到FTP服务器
使用FTP需引入lib包(commons-net-3.3.jar,下载链接见文章底部),主要使用FTPClient这个类进行文件的上传下载操作,上传需要连接登录/上传文件/断开链接三步;
连接登陆
主要代码如下代码块
/**
* 连接登录FTP服务器
*
* @return boolean 连接结果
*/
boolean ftpConnect() {
LogUtil.d("start ftpConnect");
boolean result = false;
try {
ftpClient = new FTPClient();
//set timeout
ftpClient.setConnectTimeout(TIME_OUT / 2);
ftpClient.setDataTimeout(TIME_OUT);
//start connect
ftpClient.connect(FTP_SERVER);
//get connect result
result = FTPReply.isPositiveCompletion(ftpClient.getReplyCode());
if (result) {
//set passive mode
ftpClient.enterLocalPassiveMode();
//log in
ftpClient.login(FTP_USERNAME, FTP_PASSWORD);
//set filetype, here set bytearray
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
}
} catch (Exception e) {
e.printStackTrace();
LogUtil.e("error: " + e);
} finally {
if (!result) {
ftpDisConnect();
}
}
return result;
}
这里需解释的是
- ftpClient.enterLocalPassiveMode():FTP分主动模式和被动模式,关于主动模式和被动模式在补充中做了相关的简单说明;由于很多客户端在防火墙内,开放端口给服务器端用比较困难,所以用被动模式的时候比较多;设置模式需要在登录之前设置才会生效;
- ftpClient.setFileType(FTP.BINARY_FILE_TYPE):设置文件传输方式,传输方式主要有ASCII和二进制传输两种方式,我的理解是拷贝文本文件可用ASCII方式,一般都用二进制传输方式传输;
上传文件
上传文件其实很简单,只需要调用ftpClient.storeFile方法就可以了,部分代码如下代码块
/**
* 文件上传ftp
*
* @param srcFile 待上传文件
* @param remoteFileName ftp文件名称
* @return 上传结果
*/
int uploadFile(String srcFile, String remoteFileName) {
LogUtil.d("uploadFile: " + srcFile + ";remoteFileName: " + remoteFileName);
if (srcFile == null || remoteFileName == null) {
return FtpCommonUtils.INPUT_PARAM_ERROR;
}
if (!ftpClient.isConnected()) {
LogUtil.d("ftpClient not connected!");
return FtpCommonUtils.FTP_NOT_CONNECTED;
}
FileInputStream stream = null;
boolean uploadRet = false;
try {
stream = new FileInputStream(srcFile);
uploadRet = ftpClient.storeFile(remoteFileName, stream);
stream.close();
} catch (IOException e) {
e.printStackTrace();
LogUtil.e("error: " + e);
} finally {
try {
if (stream != null) {
stream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return uploadRet ? FtpCommonUtils.RETURN_SUCCESS : FtpCommonUtils.FTP_UPLOAD_FAIL;
}
ftpClient.storeFile方法的入参第一个是服务端的文件名称,第二个是本地文件的IO流;注意的是服务端的文件名不要带中文,也不要带:(中英文都不行)等特殊字符(我就是吃了:的亏,想在名称中附上上传时间,结果查了好些时间)。
断开FTP连接
/**
* 断开FTP链接
*/
void ftpDisConnect() {
LogUtil.d("ftpDisConnect");
if (ftpClient != null && ftpClient.isConnected()) {
try {
ftpClient.logout();
ftpClient.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
此demo所需要的权限:读写sd卡 internet权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
OK了,到这里基本上就结束了,是不是很简单啊~
补充
- FTP主动模式和被动模式解释:
这两种模式发起连接的方向截然相反,主动模式是从服务器端向客户端发起连接;被动模式是客户端向服务器端发起连接。
PORT(主动)方式的连接过程是:客户端向服务器的FTP端口(默认是21)发送连接请求,服务器接受连接,建立一条命令链路。当需要传送数据时,客户端在命令链路上用PORT命令告诉服务器:“我打开了***X端口,你过来连接我”。于是服务器从20端口向客户端的***X端口发送连接请求,建立一条数据链路来传送数据。
PASV(被动)方式的连接过程是:客户端向服务器的FTP端口(默认是21)发送连接请求,服务器接受连接,建立一条命令链路。当需要传送数据时,服务器在命令链路上用PASV命令告诉客户端:“我打开了***X端口,你过来连接我”。于是客户端向服务器的***X端口发送连接请求,建立一条数据链路来传送数据。
从上面可以看出,两种方式的命令链路连接方法是一样的,而数据链路的建立方法就完全不同。 - 关于FTP的断点续传,实际上是有相关接口支持的,使用断点续传需要注意的是:1)服务器支持断点续传2)客户端要知道使用REST等一系列指令来作断点续传;断点续传所使用的方法主要是setRestartOffset设置断点位置,具体实现就让小伙伴们自己尝试了。
好了,本文到这儿就基本结束了,有什么错误的地方劳烦各位大佬评论指正!
续集
- 最近(19-11-07)又做了一个过滤系统log的需求,使用的方法代码如下:
public void saveSystemLog(File logFile, long fileLimit) {
Log.d(TAG, "saveSystemLog logFile: " + logFile + ";fileLimit: " + fileLimit);
ArrayList<String> list = new ArrayList<>();
list.add("logcat");
//logcat -v time -s *:I
list.add("-v");
list.add("time");
list.add("-s");
list.add("*:I");
ArrayList<String> clearList = new ArrayList<>();
clearList.add("logcat");
clearList.add("-c");
String[] order = list.toArray(new String[list.size()]);
String[] clearOrder = clearList.toArray(new String[clearList.size()]);
Process process = null;
try {
process = Runtime.getRuntime().exec(order);
Runtime.getRuntime().exec(clearOrder);
} catch (IOException e) {
e.printStackTrace();
}
if (process == null) {
return;
}
FileOutputStream outputStream = null;
InputStream inputStream = null;
try {
outputStream = new FileOutputStream(logFile, true);
inputStream = process.getInputStream();
int line;
byte[] buffer = new byte[1024];
while ((line = inputStream.read(buffer)) > 0 && !isStop) {
if (fileLimit > 0 && logFile.length() >= fileLimit) {
break;
}
outputStream.write(buffer, 0, line);
outputStream.flush();
}
} catch (IOException e) {
Log.d(TAG, "error: " + e);
e.printStackTrace();
} finally {
try {
if (outputStream != null) {
outputStream.close();
}
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
Log.d(TAG, "error: " + e);
e.printStackTrace();
}
}
}
使用Runtime类的exec执行adb命令获取,你可以在电脑上执行adb logcat --help查看命令使用,便于你查找你需要使用的方法;
adb logcat -v time -s *:I
-v time:需要log自带时间打印 -s为过滤使用,后面跟过滤条件(Tag:TAG级别),*代表无过滤tag,I代表过滤info级别及以上的log;
adb logcat -c : 清除日志缓存
权限说明:获取系统log目前需要程序为系统权限,即进行系统签名才可以,并且需要声明权限
<uses-permission android:name="android.permission.READ_LOGS"/>
[参考博文链接]
https://blog.csdn.net/weixin_40759186/article/details/79423396
[下载FTP-lib包链接]
链接: https://pan.baidu.com/s/1t_HqSqnmTyVDMOk8Ko_voQ&shfl=shareset 提取码: e58b