完成二次备份点击功能
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Sat Jan 31 06:16:42 GMT 2026
|
#Sat Jan 31 10:50:44 GMT 2026
|
||||||
stageCount=12
|
stageCount=12
|
||||||
libraryProject=libappbase
|
libraryProject=libappbase
|
||||||
baseVersion=15.15
|
baseVersion=15.15
|
||||||
publishVersion=15.15.11
|
publishVersion=15.15.11
|
||||||
buildCount=17
|
buildCount=20
|
||||||
baseBetaVersion=15.15.12
|
baseBetaVersion=15.15.12
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import cc.winboll.studio.libappbase.LogActivity;
|
|||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
import cc.winboll.studio.libappbase.activities.FTPBackupsActivity;
|
import cc.winboll.studio.libappbase.activities.FTPBackupsActivity;
|
||||||
|
import cc.winboll.studio.libappbase.dialogs.SFTPBackupsSettingsDialog;
|
||||||
|
import cc.winboll.studio.libappbase.models.SFTPAuthModel;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Author ZhanGSKen<zhangsken@qq.com>
|
* @Author ZhanGSKen<zhangsken@qq.com>
|
||||||
@@ -137,17 +140,35 @@ public class MainActivity extends Activity {
|
|||||||
// 启动意图(唤起浏览器)
|
// 启动意图(唤起浏览器)
|
||||||
context.startActivity(intent);
|
context.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onAboutActivity(View view) {
|
public void onAboutActivity(View view) {
|
||||||
LogUtils.d(TAG, "onAboutActivity() 调用");
|
LogUtils.d(TAG, "onAboutActivity() 调用");
|
||||||
Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class);
|
Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class);
|
||||||
startActivity(aboutIntent);
|
startActivity(aboutIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onFTPBackupsActivity(View view) {
|
public void onFTPBackupsActivity(View view) {
|
||||||
LogUtils.d(TAG, "onFTPBackupsActivity() 调用");
|
LogUtils.d(TAG, "onFTPBackupsActivity() 调用");
|
||||||
Intent ftpBackupsIntent = new Intent(getApplicationContext(), FTPBackupsActivity.class);
|
SFTPBackupsSettingsDialog dlg = new SFTPBackupsSettingsDialog(this);
|
||||||
startActivity(ftpBackupsIntent);
|
SFTPAuthModel authModel = dlg.getSFTPAuthModelFromSP(this);
|
||||||
}
|
if (authModel == null) {
|
||||||
|
dlg.show();
|
||||||
|
} else {
|
||||||
|
// 1. 构建SDCard目录待备份文件Map(与BackupUtils的SdcardMap泛型一致:String-String)
|
||||||
|
HashMap<String, String> sdcardFileMap = new HashMap<>();
|
||||||
|
// 存入文件:key=唯一标识,value=应用外部文件目录下的相对路径(与原addSdcardFile参数一致)
|
||||||
|
sdcardFileMap.put("cc.winboll.studio.libappbase.LogUtilsClassTAGBean_List.json",
|
||||||
|
"/BaseBean/cc.winboll.studio.libappbase.LogUtilsClassTAGBean_List.json");
|
||||||
|
|
||||||
|
// 2. 构建Intent,指定跳转到FTPBackupsActivity
|
||||||
|
Intent ftpBackupsIntent = new Intent(getApplicationContext(), FTPBackupsActivity.class);
|
||||||
|
// 3. 序列化传递Map参数(使用FTPBackupsActivity中定义的常量,避免硬编码)
|
||||||
|
ftpBackupsIntent.putExtra(FTPBackupsActivity.EXTRA_SDCARD_DIR_FILE_MAP, sdcardFileMap);
|
||||||
|
// 若需要传Data目录的Map,同理:ftpBackupsIntent.putExtra(FTPBackupsActivity.EXTRA_DATA_DIR_FILE_MAP, dataFileMap);
|
||||||
|
|
||||||
|
// 4. 启动Activity,参数自动透传
|
||||||
|
startActivity(ftpBackupsIntent);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#Created by .winboll/winboll_app_build.gradle
|
||||||
#Sat Jan 31 06:16:42 GMT 2026
|
#Sat Jan 31 10:50:44 GMT 2026
|
||||||
stageCount=12
|
stageCount=12
|
||||||
libraryProject=libappbase
|
libraryProject=libappbase
|
||||||
baseVersion=15.15
|
baseVersion=15.15
|
||||||
publishVersion=15.15.11
|
publishVersion=15.15.11
|
||||||
buildCount=17
|
buildCount=20
|
||||||
baseBetaVersion=15.15.12
|
baseBetaVersion=15.15.12
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
package cc.winboll.studio.libappbase.activities;
|
package cc.winboll.studio.libappbase.activities;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
import cc.winboll.studio.libappbase.R;
|
import cc.winboll.studio.libappbase.R;
|
||||||
import cc.winboll.studio.libappbase.ToastUtils;
|
import cc.winboll.studio.libappbase.ToastUtils;
|
||||||
@@ -15,8 +21,10 @@ import cc.winboll.studio.libappbase.dialogs.SFTPBackupsSettingsDialog;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* BackupUtils 调用实例
|
* BackupUtils 调用实例
|
||||||
|
* 支持Intent传入双Map参数,初始化BackupUtils的待备份文件列表
|
||||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
* @Date 2026/01/30 20:55
|
* @Date 2026/01/30 20:55
|
||||||
|
* @LastEditTime 2026/02/01 04:00
|
||||||
*/
|
*/
|
||||||
public class FTPBackupsActivity extends Activity {
|
public class FTPBackupsActivity extends Activity {
|
||||||
|
|
||||||
@@ -26,72 +34,114 @@ public class FTPBackupsActivity extends Activity {
|
|||||||
// FTP服务器上传目标目录(可根据业务自定义)
|
// FTP服务器上传目标目录(可根据业务自定义)
|
||||||
private static final String FTP_TARGET_DIR = "/WinBoLLStudio/APPBackups/WinBoLL/";
|
private static final String FTP_TARGET_DIR = "/WinBoLLStudio/APPBackups/WinBoLL/";
|
||||||
|
|
||||||
|
// ==================== Intent传参常量(规范外部调用)====================
|
||||||
|
/**
|
||||||
|
* Intent传入参数-Data目录待备份文件Map
|
||||||
|
* 类型:HashMap<String,String> 实现Serializable接口
|
||||||
|
*/
|
||||||
|
public static final String EXTRA_DATA_DIR_FILE_MAP = "extra_data_dir_file_map";
|
||||||
|
/**
|
||||||
|
* Intent传入参数-SDCard目录待备份文件Map
|
||||||
|
* 类型:HashMap<String,String> 实现Serializable接口
|
||||||
|
*/
|
||||||
|
public static final String EXTRA_SDCARD_DIR_FILE_MAP = "extra_sdcard_dir_file_map";
|
||||||
|
|
||||||
|
// 解析后的双Map参数(用于初始化BackupUtils)
|
||||||
|
private Map<String, String> mDataDirFileMap;
|
||||||
|
private Map<String, String> mSdcardDirFileMap;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_ftp_backups);
|
setContentView(R.layout.activity_ftp_backups);
|
||||||
// 初始化主线程Handler,避免子线程更新UI崩溃
|
// 初始化主线程Handler,避免子线程更新UI崩溃
|
||||||
mMainHandler = new Handler(Looper.getMainLooper());
|
mMainHandler = new Handler(Looper.getMainLooper());
|
||||||
|
// 解析Intent中的双Map参数
|
||||||
|
parseIntentParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onSFTPSettings(View view) {
|
/**
|
||||||
|
* 解析Intent传入的序列化Map参数
|
||||||
|
* 兜底处理:参数为空/类型错误时,赋值为null(兼容BackupUtils初始化逻辑)
|
||||||
|
*/
|
||||||
|
private void parseIntentParams() {
|
||||||
|
Intent intent = getIntent();
|
||||||
|
if (intent != null) {
|
||||||
|
// 解析Data目录Map:强转为HashMap(实现Serializable,可序列化传递)
|
||||||
|
Serializable dataMapSer = intent.getSerializableExtra(EXTRA_DATA_DIR_FILE_MAP);
|
||||||
|
if (dataMapSer instanceof HashMap) {
|
||||||
|
mDataDirFileMap = (HashMap<String, String>) dataMapSer;
|
||||||
|
LogUtils.d(TAG, "Intent解析Data目录Map成功,共" + mDataDirFileMap.size() + "个文件");
|
||||||
|
} else {
|
||||||
|
mDataDirFileMap = null;
|
||||||
|
LogUtils.d(TAG, "Intent未解析到有效Data目录Map");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析SDCard目录Map:强转为HashMap(实现Serializable,可序列化传递)
|
||||||
|
Serializable sdcardMapSer = intent.getSerializableExtra(EXTRA_SDCARD_DIR_FILE_MAP);
|
||||||
|
if (sdcardMapSer instanceof HashMap) {
|
||||||
|
mSdcardDirFileMap = (HashMap<String, String>) sdcardMapSer;
|
||||||
|
LogUtils.d(TAG, "Intent解析SDCard目录Map成功,共" + mSdcardDirFileMap.size() + "个文件");
|
||||||
|
} else {
|
||||||
|
mSdcardDirFileMap = null;
|
||||||
|
LogUtils.d(TAG, "Intent未解析到有效SDCard目录Map");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 未解析到参数时,保持null即可,BackupUtils内部会默认初始化空Map
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onSFTPSettings(View view) {
|
||||||
SFTPBackupsSettingsDialog dlg = new SFTPBackupsSettingsDialog(this);
|
SFTPBackupsSettingsDialog dlg = new SFTPBackupsSettingsDialog(this);
|
||||||
dlg.show();
|
dlg.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 点击事件:执行FTP备份(核心调用方法)
|
* 点击事件:执行FTP备份(核心调用方法)
|
||||||
* 主线程仅做UI触发,所有核心逻辑在子线程执行
|
* 主线程仅做UI触发,所有核心逻辑在子线程执行
|
||||||
|
* 每次点击都重新解析Intent参数,保证获取最新的文件列表
|
||||||
*/
|
*/
|
||||||
public void onBackups(View view) {
|
public void onBackups(View view) {
|
||||||
ToastUtils.show("开始执行FTP备份,请勿退出页面...");
|
ToastUtils.show("开始执行FTP备份,请勿退出页面...");
|
||||||
LogUtils.d(TAG, "触发FTP备份操作,开启子线程执行核心逻辑");
|
LogUtils.d(TAG, "触发FTP备份操作,开启子线程执行核心逻辑");
|
||||||
SFTPBackupsSettingsDialog dlg = new SFTPBackupsSettingsDialog(this);
|
// 每次点击都重新解析Intent参数,避免Activity复用导致参数失效
|
||||||
SFTPAuthModel authModel = dlg.getSFTPAuthModelFromSP(this);
|
parseIntentParams();
|
||||||
if (authModel == null) {
|
SFTPBackupsSettingsDialog dlg = new SFTPBackupsSettingsDialog(this);
|
||||||
dlg.show();
|
SFTPAuthModel authModel = dlg.getSFTPAuthModelFromSP(this);
|
||||||
} else {
|
if (authModel == null) {
|
||||||
doBackups(authModel);
|
dlg.show();
|
||||||
}
|
} else {
|
||||||
|
// 传入解析后的双Map参数
|
||||||
|
doBackups(authModel, mDataDirFileMap, mSdcardDirFileMap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void doBackups(final SFTPAuthModel authModel) {
|
/**
|
||||||
|
* 核心备份逻辑:子线程执行
|
||||||
|
* @param authModel SFTP认证模型
|
||||||
|
* @param dataDirFileMap Intent解析的Data目录文件Map
|
||||||
|
* @param sdcardDirFileMap Intent解析的SDCard目录文件Map
|
||||||
|
*/
|
||||||
|
void doBackups(final SFTPAuthModel authModel, final Map<String, String> dataDirFileMap, final Map<String, String> sdcardDirFileMap) {
|
||||||
// 所有BackupUtils操作放入子线程,规避网络/IO主线程异常
|
// 所有BackupUtils操作放入子线程,规避网络/IO主线程异常
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
// ================================= 步骤1:构建FTP登录配置 =================================
|
// 初始化BackupUtils单例,透传解析后的双Map参数
|
||||||
|
|
||||||
|
|
||||||
// ================================= 步骤2:初始化BackupUtils单例 =================================
|
|
||||||
// 推荐传Application上下文,这里临时传Activity(实际开发替换为getApplicationContext())
|
|
||||||
BackupUtils backupUtils = BackupUtils.getInstance(
|
BackupUtils backupUtils = BackupUtils.getInstance(
|
||||||
FTPBackupsActivity.this,
|
getApplicationContext(),
|
||||||
authModel,
|
authModel,
|
||||||
FTP_TARGET_DIR // FTP服务器上传目录
|
FTP_TARGET_DIR,
|
||||||
|
dataDirFileMap,
|
||||||
|
sdcardDirFileMap
|
||||||
);
|
);
|
||||||
|
|
||||||
// ================================= 步骤3:添加待备份文件(Data目录+SDCard目录) =================================
|
LogUtils.d(TAG, "待备份文件初始化完成 → Data目录:" + backupUtils.getAllDataDirFiles().size() + "个 | SDCard目录:" + backupUtils.getAllSdcardFiles().size() + "个");
|
||||||
// 【应用Data目录】添加文件:key=唯一标识,value=Data目录下的相对路径
|
|
||||||
// 示例:Data/files/logs/app.log 、 Data/cache/error.log
|
|
||||||
//backupUtils.addDataDirFile("app_log", "files/logs/app.log");
|
|
||||||
//backupUtils.addDataDirFile("error_log", "cache/error.log");
|
|
||||||
|
|
||||||
// 【SDCard目录】添加文件:key=唯一标识,value=SDCard目录下的相对路径
|
// 核心执行:打包为ZIP + SFTP上传(内部已实现登录→打包→上传→登出)
|
||||||
// 示例:/sdcard/winboll/backup/crash.log 、 /sdcard/test.apk
|
|
||||||
//backupUtils.addSdcardFile("crash_log", "winboll/backup/crash.log");
|
|
||||||
//backupUtils.addSdcardFile("test_apk", "test.apk"); // 对应/sdcard/test.apk
|
|
||||||
backupUtils.addSdcardFile("cc.winboll.studio.libappbase.LogUtilsClassTAGBean_List.json", "/BaseBean/cc.winboll.studio.libappbase.LogUtilsClassTAGBean_List.json"); // 对应/sdcard/test.apk
|
|
||||||
|
|
||||||
LogUtils.d(TAG, "待备份文件添加完成 → Data目录:" + backupUtils.getAllDataDirFiles().size() + "个 | SDCard目录:" + backupUtils.getAllSdcardFiles().size() + "个");
|
|
||||||
|
|
||||||
// ================================= 步骤4:核心执行:打包为ZIP + FTP上传 =================================
|
|
||||||
// 该方法已内置:FTP登录→ZIP打包→FTP上传→FTP登出,全程子线程执行
|
|
||||||
boolean isSuccess = backupUtils.packAndUploadByFtp();
|
boolean isSuccess = backupUtils.packAndUploadByFtp();
|
||||||
|
|
||||||
// ================================= 步骤5:主线程反馈执行结果 =================================
|
// 主线程反馈执行结果
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
updateUi("FTP备份成功!文件已打包为ZIP上传至服务器:" + FTP_TARGET_DIR);
|
updateUi("FTP备份成功!文件已打包为ZIP上传至服务器:" + FTP_TARGET_DIR);
|
||||||
LogUtils.i(TAG, "FTP备份全流程执行成功");
|
LogUtils.i(TAG, "FTP备份全流程执行成功");
|
||||||
@@ -109,18 +159,14 @@ public class FTPBackupsActivity extends Activity {
|
|||||||
LogUtils.e(TAG, "FTP备份执行异常:" + e.getMessage(), e);
|
LogUtils.e(TAG, "FTP备份执行异常:" + e.getMessage(), e);
|
||||||
updateUi("备份执行失败:" + e.getMessage());
|
updateUi("备份执行失败:" + e.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
// 兜底:清空BackupUtils的文件列表(避免重复添加)
|
// 仅保留FTP断连,BackupUtils内部已兜底登出/断连,此处做双重保障
|
||||||
if (BackupUtils.getInstance() != null) {
|
FTPUtils.getInstance().disconnect();
|
||||||
BackupUtils.getInstance().clearDataDirFiles();
|
// 关键日志:明确仅删除临时ZIP包,不修改/删除源文件
|
||||||
BackupUtils.getInstance().clearSdcardFiles();
|
LogUtils.d(TAG, "备份流程结束:仅删除本地临时ZIP打包文件,待备份源文件未做任何修改/删除");
|
||||||
LogUtils.d(TAG, "兜底清空待备份文件列表");
|
|
||||||
}
|
|
||||||
// 兜底:断开FTP连接,释放所有资源
|
|
||||||
FTPUtils.getInstance().logout();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 子线程更新UI工具方法(Toast提示)
|
* 子线程更新UI工具方法(Toast提示)
|
||||||
@@ -137,6 +183,7 @@ public class FTPBackupsActivity extends Activity {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 页面销毁:释放资源,避免内存泄漏
|
* 页面销毁:释放资源,避免内存泄漏
|
||||||
|
* 仅释放本地资源,不清空Map/重复断连FTP
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
@@ -145,8 +192,10 @@ public class FTPBackupsActivity extends Activity {
|
|||||||
if (mMainHandler != null) {
|
if (mMainHandler != null) {
|
||||||
mMainHandler.removeCallbacksAndMessages(null);
|
mMainHandler.removeCallbacksAndMessages(null);
|
||||||
}
|
}
|
||||||
// 断开FTP连接,释放BackupUtils相关资源
|
// 清空本地Map引用,协助GC(不影响BackupUtils内部逻辑)
|
||||||
FTPUtils.getInstance().logout();
|
mDataDirFileMap = null;
|
||||||
|
mSdcardDirFileMap = null;
|
||||||
|
LogUtils.d(TAG, "FTPBackupsActivity销毁:已释放本地资源,不影响二次备份初始化");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import cc.winboll.studio.libappbase.models.SFTPAuthModel;
|
|||||||
* 兼容:Java7、Android 6.0+,无第三方依赖(ZIP为原生实现),免动态读写权限
|
* 兼容:Java7、Android 6.0+,无第三方依赖(ZIP为原生实现),免动态读写权限
|
||||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
* @Date 2026/01/30 20:18:00
|
* @Date 2026/01/30 20:18:00
|
||||||
* @LastEditTime 2026/02/01 01:05:00
|
* @LastEditTime 2026/02/01 02:05:00
|
||||||
*/
|
*/
|
||||||
public class BackupUtils {
|
public class BackupUtils {
|
||||||
public static final String TAG = "BackupUtils";
|
public static final String TAG = "BackupUtils";
|
||||||
@@ -50,29 +50,41 @@ public class BackupUtils {
|
|||||||
// 应用专属外部文件目录(SDCard Map的基础根目录,初始化时赋值,避免重复创建)
|
// 应用专属外部文件目录(SDCard Map的基础根目录,初始化时赋值,避免重复创建)
|
||||||
private File mAppExternalFilesDir;
|
private File mAppExternalFilesDir;
|
||||||
|
|
||||||
// 私有构造器:禁止外部实例化,初始化双Map+配置参数+标准化SFTP上传目录
|
// 私有构造器:新增双Map入参,空值则使用内部默认初始化,非空则用入参初始化
|
||||||
private BackupUtils(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir) {
|
private BackupUtils(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir,
|
||||||
|
Map<String, String> dataDirFileMap, Map<String, String> sdcardFileMap) {
|
||||||
this.mAppContext = context.getApplicationContext();
|
this.mAppContext = context.getApplicationContext();
|
||||||
this.mFtpAuthModel = ftpAuthModel;
|
this.mFtpAuthModel = ftpAuthModel;
|
||||||
// 初始化SDCard Map的基础根目录:应用专属外部文件目录(/storage/emulated/0/Android/data/[包名]/files)
|
// 初始化SDCard Map的基础根目录:应用专属外部文件目录(/storage/emulated/0/Android/data/[包名]/files)
|
||||||
this.mAppExternalFilesDir = mAppContext.getExternalFilesDir(null);
|
this.mAppExternalFilesDir = mAppContext.getExternalFilesDir(null);
|
||||||
// 标准化SFTP上传目录:空则默认/,非空则补全结尾斜杠
|
// 标准化SFTP上传目录:空则默认/,非空则补全结尾斜杠
|
||||||
this.mFtpTargetDir = TextUtils.isEmpty(ftpTargetDir) ? "/" : (ftpTargetDir.endsWith("/") ? ftpTargetDir : ftpTargetDir + "/");
|
this.mFtpTargetDir = TextUtils.isEmpty(ftpTargetDir) ? "/" : (ftpTargetDir.endsWith("/") ? ftpTargetDir : ftpTargetDir + "/");
|
||||||
// 初始化双Map(HashMap兼容Java7)
|
|
||||||
mDataDirFileMap = new HashMap<>();
|
// 核心修改:入参Map非空且非空集合时,使用入参初始化;否则内部new HashMap()
|
||||||
mSdcardFileMap = new HashMap<>();
|
this.mDataDirFileMap = (dataDirFileMap != null && !dataDirFileMap.isEmpty())
|
||||||
|
? new HashMap<>(dataDirFileMap) // 新建Map避免外部篡改内部数据
|
||||||
|
: new HashMap<>();
|
||||||
|
this.mSdcardFileMap = (sdcardFileMap != null && !sdcardFileMap.isEmpty())
|
||||||
|
? new HashMap<>(sdcardFileMap) // 深拷贝,隔离外部引用
|
||||||
|
: new HashMap<>();
|
||||||
|
|
||||||
LogUtils.d(TAG, "BackupUtils初始化完成 → SFTP服务器:" + ftpAuthModel.getFtpServer() + ":" + ftpAuthModel.getFtpPort() + " | 上传目录:" + mFtpTargetDir);
|
LogUtils.d(TAG, "BackupUtils初始化完成 → SFTP服务器:" + ftpAuthModel.getFtpServer() + ":" + ftpAuthModel.getFtpPort() + " | 上传目录:" + mFtpTargetDir);
|
||||||
LogUtils.d(TAG, "SDCard Map基础根目录:" + (mAppExternalFilesDir == null ? "获取失败" : mAppExternalFilesDir.getAbsolutePath()));
|
LogUtils.d(TAG, "SDCard Map基础根目录:" + (mAppExternalFilesDir == null ? "获取失败" : mAppExternalFilesDir.getAbsolutePath()));
|
||||||
|
LogUtils.d(TAG, "初始化后DataMap大小:" + mDataDirFileMap.size() + " | SdcardMap大小:" + mSdcardFileMap.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单例初始化方法(必须先调用,否则getInstance()会抛异常)
|
* 单例初始化方法(必须先调用,否则getInstance()会抛异常)
|
||||||
|
* 新增双Map入参,支持外部初始化待备份文件列表
|
||||||
* @param context 上下文(推荐传Application,避免内存泄漏)
|
* @param context 上下文(推荐传Application,避免内存泄漏)
|
||||||
* @param ftpAuthModel 外部SFTP认证实体类(含服务器/账号/端口等)
|
* @param ftpAuthModel 外部SFTP认证实体类(含服务器/账号/端口等)
|
||||||
* @param ftpTargetDir SFTP服务器指定上传目录(如/backup,自动补全斜杠)
|
* @param ftpTargetDir SFTP服务器指定上传目录(如/backup,自动补全斜杠)
|
||||||
|
* @param dataDirFileMap 外部传入的Data目录文件Map,null/空则内部默认初始化
|
||||||
|
* @param sdcardFileMap 外部传入的SDCard目录文件Map,null/空则内部默认初始化
|
||||||
* @return BackupUtils单例实例
|
* @return BackupUtils单例实例
|
||||||
*/
|
*/
|
||||||
public static BackupUtils getInstance(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir) {
|
public static BackupUtils getInstance(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir,
|
||||||
|
Map<String, String> dataDirFileMap, Map<String, String> sdcardFileMap) {
|
||||||
if (sInstance == null) {
|
if (sInstance == null) {
|
||||||
synchronized (BackupUtils.class) {
|
synchronized (BackupUtils.class) {
|
||||||
if (sInstance == null) {
|
if (sInstance == null) {
|
||||||
@@ -83,30 +95,34 @@ public class BackupUtils {
|
|||||||
if (ftpAuthModel == null || TextUtils.isEmpty(ftpAuthModel.getFtpServer())) {
|
if (ftpAuthModel == null || TextUtils.isEmpty(ftpAuthModel.getFtpServer())) {
|
||||||
throw new IllegalArgumentException("初始化失败:SFTPAuthModel/ftpServer 不能为空");
|
throw new IllegalArgumentException("初始化失败:SFTPAuthModel/ftpServer 不能为空");
|
||||||
}
|
}
|
||||||
sInstance = new BackupUtils(context, ftpAuthModel, ftpTargetDir);
|
// 透传新增的双Map入参至构造器
|
||||||
|
sInstance = new BackupUtils(context, ftpAuthModel, ftpTargetDir, dataDirFileMap, sdcardFileMap);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sInstance;
|
return sInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重载默认初始化方法:兼容原有调用逻辑,无需传入Map,内部默认初始化
|
||||||
|
* 避免修改后影响原有代码调用
|
||||||
|
*/
|
||||||
|
public static BackupUtils getInstance(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir) {
|
||||||
|
return getInstance(context, ftpAuthModel, ftpTargetDir, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取单例实例(需先调用带参getInstance初始化)
|
* 获取单例实例(需先调用带参getInstance初始化)
|
||||||
* @return BackupUtils单例实例
|
* @return BackupUtils单例实例
|
||||||
*/
|
*/
|
||||||
public static BackupUtils getInstance() {
|
public static BackupUtils getInstance() {
|
||||||
if (sInstance == null) {
|
if (sInstance == null) {
|
||||||
throw new IllegalStateException("BackupUtils未初始化,请先调用getInstance(Context, SFTPAuthModel, String)");
|
throw new IllegalStateException("BackupUtils未初始化,请先调用getInstance(Context, SFTPAuthModel, String[, Map, Map])");
|
||||||
}
|
}
|
||||||
return sInstance;
|
return sInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================================== 应用Data目录 - Map操作方法 ======================================
|
// ====================================== 以下原有方法均未修改 ======================================
|
||||||
/**
|
|
||||||
* 添加应用Data目录下的备份文件(相对路径)
|
|
||||||
* @param key 文件唯一标识(如log_20260130,避免重复)
|
|
||||||
* @param relativePath Data目录下的相对路径,如:log/app.log
|
|
||||||
*/
|
|
||||||
public void addDataDirFile(String key, String relativePath) {
|
public void addDataDirFile(String key, String relativePath) {
|
||||||
if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(relativePath)) {
|
if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(relativePath)) {
|
||||||
mDataDirFileMap.put(key, relativePath);
|
mDataDirFileMap.put(key, relativePath);
|
||||||
@@ -114,10 +130,6 @@ public class BackupUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除应用Data目录下的指定备份文件
|
|
||||||
* @param key 文件唯一标识
|
|
||||||
*/
|
|
||||||
public void removeDataDirFile(String key) {
|
public void removeDataDirFile(String key) {
|
||||||
if (!TextUtils.isEmpty(key) && mDataDirFileMap.containsKey(key)) {
|
if (!TextUtils.isEmpty(key) && mDataDirFileMap.containsKey(key)) {
|
||||||
mDataDirFileMap.remove(key);
|
mDataDirFileMap.remove(key);
|
||||||
@@ -125,37 +137,19 @@ public class BackupUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取应用Data目录下指定标识的文件相对路径
|
|
||||||
* @param key 文件唯一标识
|
|
||||||
* @return 相对路径,无则返回null
|
|
||||||
*/
|
|
||||||
public String getDataDirFile(String key) {
|
public String getDataDirFile(String key) {
|
||||||
return mDataDirFileMap.get(key);
|
return mDataDirFileMap.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取Data目录下所有备份文件(返回新Map,防止外部篡改原数据)
|
|
||||||
* @return 只读Map副本
|
|
||||||
*/
|
|
||||||
public Map<String, String> getAllDataDirFiles() {
|
public Map<String, String> getAllDataDirFiles() {
|
||||||
return new HashMap<>(mDataDirFileMap);
|
return new HashMap<>(mDataDirFileMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空应用Data目录下的所有备份文件
|
|
||||||
*/
|
|
||||||
public void clearDataDirFiles() {
|
public void clearDataDirFiles() {
|
||||||
mDataDirFileMap.clear();
|
mDataDirFileMap.clear();
|
||||||
LogUtils.d(TAG, "清空Data目录所有备份文件");
|
LogUtils.d(TAG, "清空Data目录所有备份文件");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================================== 应用专属外部文件目录 - Map操作方法 ======================================
|
|
||||||
/**
|
|
||||||
* 添加应用专属外部文件目录下的备份文件(相对路径)
|
|
||||||
* @param key 文件唯一标识(如crash_20260130,避免重复)
|
|
||||||
* @param relativePath 外部文件目录下的相对路径,如:backup/crash.log
|
|
||||||
*/
|
|
||||||
public void addSdcardFile(String key, String relativePath) {
|
public void addSdcardFile(String key, String relativePath) {
|
||||||
if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(relativePath) && mAppExternalFilesDir != null) {
|
if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(relativePath) && mAppExternalFilesDir != null) {
|
||||||
mSdcardFileMap.put(key, relativePath);
|
mSdcardFileMap.put(key, relativePath);
|
||||||
@@ -163,10 +157,6 @@ public class BackupUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除应用专属外部文件目录下的指定备份文件
|
|
||||||
* @param key 文件唯一标识
|
|
||||||
*/
|
|
||||||
public void removeSdcardFile(String key) {
|
public void removeSdcardFile(String key) {
|
||||||
if (!TextUtils.isEmpty(key) && mSdcardFileMap.containsKey(key)) {
|
if (!TextUtils.isEmpty(key) && mSdcardFileMap.containsKey(key)) {
|
||||||
mSdcardFileMap.remove(key);
|
mSdcardFileMap.remove(key);
|
||||||
@@ -174,41 +164,20 @@ public class BackupUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取应用专属外部文件目录下指定标识的文件相对路径
|
|
||||||
* @param key 文件唯一标识
|
|
||||||
* @return 相对路径,无则返回null
|
|
||||||
*/
|
|
||||||
public String getSdcardFile(String key) {
|
public String getSdcardFile(String key) {
|
||||||
return mSdcardFileMap.get(key);
|
return mSdcardFileMap.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取应用专属外部文件目录下所有备份文件(返回新Map,防止外部篡改原数据)
|
|
||||||
* @return 只读Map副本
|
|
||||||
*/
|
|
||||||
public Map<String, String> getAllSdcardFiles() {
|
public Map<String, String> getAllSdcardFiles() {
|
||||||
return new HashMap<>(mSdcardFileMap);
|
return new HashMap<>(mSdcardFileMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空应用专属外部文件目录下的所有备份文件
|
|
||||||
*/
|
|
||||||
public void clearSdcardFiles() {
|
public void clearSdcardFiles() {
|
||||||
mSdcardFileMap.clear();
|
mSdcardFileMap.clear();
|
||||||
LogUtils.d(TAG, "清空外部文件目录所有备份文件");
|
LogUtils.d(TAG, "清空外部文件目录所有备份文件");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================================== 核心方法:SFTP分步式打包上传(登录→传输→登出) ======================================
|
|
||||||
/**
|
|
||||||
* 核心方法:双Map文件打包为ZIP(分data/sdcard目录,UUID+时间戳)+ SFTP分步上传
|
|
||||||
* 执行流程:1.SFTP登录 → 2.本地ZIP打包 → 3.SFTP文件上传 → 4.SFTP登出
|
|
||||||
* 每步独立校验,异常即时返回,finally兜底释放所有资源
|
|
||||||
* @return true=全流程成功,false=任意步骤失败
|
|
||||||
* 注:必须在**子线程**执行(避免主线程阻塞ANR)
|
|
||||||
*/
|
|
||||||
public boolean packAndUploadByFtp() {
|
public boolean packAndUploadByFtp() {
|
||||||
// 前置校验:无待备份文件/外部文件目录获取失败,直接返回失败
|
|
||||||
if (mDataDirFileMap.isEmpty() && mSdcardFileMap.isEmpty()) {
|
if (mDataDirFileMap.isEmpty() && mSdcardFileMap.isEmpty()) {
|
||||||
LogUtils.e(TAG, "SFTP上传失败:无待备份文件(DataDir+外部文件目录均为空)");
|
LogUtils.e(TAG, "SFTP上传失败:无待备份文件(DataDir+外部文件目录均为空)");
|
||||||
return false;
|
return false;
|
||||||
@@ -218,21 +187,15 @@ public class BackupUtils {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 生成ZIP文件名(UUID去横杠 + 毫秒时间戳,保证全球唯一)
|
|
||||||
String zipFileName = UUID.randomUUID().toString().replace("-", "")
|
String zipFileName = UUID.randomUUID().toString().replace("-", "")
|
||||||
+ "-" + System.currentTimeMillis() + ".zip";
|
+ "-" + System.currentTimeMillis() + ".zip";
|
||||||
// 本地临时ZIP文件:应用外部缓存目录(Android 6.0+免读写权限)
|
|
||||||
File tempZipFile = new File(mAppContext.getExternalCacheDir(), zipFileName);
|
File tempZipFile = new File(mAppContext.getExternalCacheDir(), zipFileName);
|
||||||
// SFTP远程完整路径:标准化上传目录 + ZIP文件名(已提前补全斜杠,直接拼接)
|
|
||||||
String remoteFtpFilePath = mFtpTargetDir + zipFileName;
|
String remoteFtpFilePath = mFtpTargetDir + zipFileName;
|
||||||
|
|
||||||
// 2. 获取FTPUtils单例(全局唯一)
|
|
||||||
FTPUtils ftpUtils = FTPUtils.getInstance();
|
FTPUtils ftpUtils = FTPUtils.getInstance();
|
||||||
// 上传结果标记
|
|
||||||
boolean isUploadSuccess = false;
|
boolean isUploadSuccess = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ==================== 第一步:SFTP服务器登录(基于外部SFTPAuthModel)====================
|
|
||||||
LogUtils.d(TAG, "开始SFTP登录:" + mFtpAuthModel.getFtpServer() + ":" + mFtpAuthModel.getFtpPort());
|
LogUtils.d(TAG, "开始SFTP登录:" + mFtpAuthModel.getFtpServer() + ":" + mFtpAuthModel.getFtpPort());
|
||||||
boolean isFtpLogin = ftpUtils.login(mFtpAuthModel);
|
boolean isFtpLogin = ftpUtils.login(mFtpAuthModel);
|
||||||
if (!isFtpLogin) {
|
if (!isFtpLogin) {
|
||||||
@@ -241,7 +204,6 @@ public class BackupUtils {
|
|||||||
}
|
}
|
||||||
LogUtils.i(TAG, "SFTP登录成功,准备打包文件:" + zipFileName);
|
LogUtils.i(TAG, "SFTP登录成功,准备打包文件:" + zipFileName);
|
||||||
|
|
||||||
// ==================== 第二步:本地打包双Map文件为ZIP(分data/sdcard目录,Java7原生实现)====================
|
|
||||||
LogUtils.d(TAG, "开始本地ZIP打包(分data/sdcard目录),临时文件路径:" + tempZipFile.getAbsolutePath());
|
LogUtils.d(TAG, "开始本地ZIP打包(分data/sdcard目录),临时文件路径:" + tempZipFile.getAbsolutePath());
|
||||||
boolean isPackSuccess = packFilesToZip(tempZipFile);
|
boolean isPackSuccess = packFilesToZip(tempZipFile);
|
||||||
if (!isPackSuccess || !tempZipFile.exists() || tempZipFile.length() == 0) {
|
if (!isPackSuccess || !tempZipFile.exists() || tempZipFile.length() == 0) {
|
||||||
@@ -250,7 +212,6 @@ public class BackupUtils {
|
|||||||
}
|
}
|
||||||
LogUtils.i(TAG, "ZIP打包成功,文件大小:" + tempZipFile.length() / 1024 + "KB");
|
LogUtils.i(TAG, "ZIP打包成功,文件大小:" + tempZipFile.length() / 1024 + "KB");
|
||||||
|
|
||||||
// ==================== 第三步:SFTP文件上传(调用现有FTPUtils.uploadFile)====================
|
|
||||||
LogUtils.d(TAG, "开始SFTP上传:本地→SFTP" + remoteFtpFilePath);
|
LogUtils.d(TAG, "开始SFTP上传:本地→SFTP" + remoteFtpFilePath);
|
||||||
isUploadSuccess = ftpUtils.uploadFile(tempZipFile.getAbsolutePath(), remoteFtpFilePath);
|
isUploadSuccess = ftpUtils.uploadFile(tempZipFile.getAbsolutePath(), remoteFtpFilePath);
|
||||||
if (isUploadSuccess) {
|
if (isUploadSuccess) {
|
||||||
@@ -260,47 +221,33 @@ public class BackupUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 捕获所有运行时异常,避免崩溃
|
|
||||||
LogUtils.e(TAG, "SFTP上传异常:" + e.getMessage(), e);
|
LogUtils.e(TAG, "SFTP上传异常:" + e.getMessage(), e);
|
||||||
isUploadSuccess = false;
|
isUploadSuccess = false;
|
||||||
} finally {
|
} finally {
|
||||||
// ==================== 最终兜底:无论成功/失败,释放所有资源 ====================
|
|
||||||
// 1. SFTP登出+断开连接(避免服务端连接数耗尽)
|
|
||||||
if (ftpUtils.isConnected()) {
|
if (ftpUtils.isConnected()) {
|
||||||
ftpUtils.logout();
|
ftpUtils.logout();
|
||||||
}
|
}
|
||||||
ftpUtils.disconnect();
|
ftpUtils.disconnect();
|
||||||
// 2. 删除本地临时ZIP文件(释放存储空间,避免缓存堆积)
|
|
||||||
if (tempZipFile.exists()) {
|
if (tempZipFile.exists()) {
|
||||||
boolean isDelete = tempZipFile.delete();
|
boolean isDelete = tempZipFile.delete();
|
||||||
LogUtils.d(TAG, "本地临时ZIP文件删除:" + (isDelete ? "成功" : "失败"));
|
LogUtils.d(TAG, "本地临时ZIP文件删除:" + (isDelete ? "成功" : "失败"));
|
||||||
}
|
}
|
||||||
// 3. 清空临时变量,协助GC
|
|
||||||
System.gc();
|
System.gc();
|
||||||
}
|
}
|
||||||
|
|
||||||
return isUploadSuccess;
|
return isUploadSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================================== 私有工具方法:ZIP打包/目录校验 ======================================
|
|
||||||
/**
|
|
||||||
* Java7原生ZIP打包:分目录打包(data/sdcard),遍历双Map文件写入ZIP
|
|
||||||
* @param zipFile 生成的临时ZIP文件
|
|
||||||
* @return true=打包成功,false=打包失败
|
|
||||||
*/
|
|
||||||
private boolean packFilesToZip(File zipFile) {
|
private boolean packFilesToZip(File zipFile) {
|
||||||
ZipOutputStream zos = null;
|
ZipOutputStream zos = null;
|
||||||
try {
|
try {
|
||||||
// 初始化ZIP输出流,UTF-8编码解决中文文件名乱码
|
|
||||||
zos = new ZipOutputStream(new FileOutputStream(zipFile), Charset.forName("UTF-8"));
|
zos = new ZipOutputStream(new FileOutputStream(zipFile), Charset.forName("UTF-8"));
|
||||||
zos.setLevel(ZipOutputStream.DEFLATED); // 开启压缩,减小文件体积
|
zos.setLevel(ZipOutputStream.DEFLATED);
|
||||||
|
|
||||||
// 1. 打包应用Data目录下的文件 → ZIP内的data子目录
|
|
||||||
if (!mDataDirFileMap.isEmpty()) {
|
if (!mDataDirFileMap.isEmpty()) {
|
||||||
packDirFilesToZip(zos, mDataDirFileMap, mAppContext.getFilesDir(), ZIP_DIR_DATA);
|
packDirFilesToZip(zos, mDataDirFileMap, mAppContext.getFilesDir(), ZIP_DIR_DATA);
|
||||||
LogUtils.d(TAG, "Data目录文件已打包到ZIP→" + ZIP_DIR_DATA + "子目录");
|
LogUtils.d(TAG, "Data目录文件已打包到ZIP→" + ZIP_DIR_DATA + "子目录");
|
||||||
}
|
}
|
||||||
// 2. 打包应用专属外部文件目录下的文件 → ZIP内的sdcard子目录
|
|
||||||
if (!mSdcardFileMap.isEmpty() && mAppExternalFilesDir != null) {
|
if (!mSdcardFileMap.isEmpty() && mAppExternalFilesDir != null) {
|
||||||
packDirFilesToZip(zos, mSdcardFileMap, mAppExternalFilesDir, ZIP_DIR_SDCARD);
|
packDirFilesToZip(zos, mSdcardFileMap, mAppExternalFilesDir, ZIP_DIR_SDCARD);
|
||||||
LogUtils.d(TAG, "应用专属外部文件目录文件已打包到ZIP→" + ZIP_DIR_SDCARD + "子目录");
|
LogUtils.d(TAG, "应用专属外部文件目录文件已打包到ZIP→" + ZIP_DIR_SDCARD + "子目录");
|
||||||
@@ -312,7 +259,6 @@ public class BackupUtils {
|
|||||||
LogUtils.e(TAG, "ZIP打包IO异常:" + e.getMessage(), e);
|
LogUtils.e(TAG, "ZIP打包IO异常:" + e.getMessage(), e);
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
// 关闭ZIP流,释放资源
|
|
||||||
if (zos != null) {
|
if (zos != null) {
|
||||||
try {
|
try {
|
||||||
zos.close();
|
zos.close();
|
||||||
@@ -323,29 +269,18 @@ public class BackupUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量打包指定目录下的文件到ZIP指定子目录
|
|
||||||
* @param zos ZIP输出流
|
|
||||||
* @param fileMap 待打包文件Map(key=标识,value=相对路径)
|
|
||||||
* @param baseDir 本地基础根目录(Data/应用专属外部文件目录)
|
|
||||||
* @param zipSubDir ZIP内的目标子目录(如data/、sdcard/)
|
|
||||||
*/
|
|
||||||
private void packDirFilesToZip(ZipOutputStream zos, Map<String, String> fileMap, File baseDir, String zipSubDir) {
|
private void packDirFilesToZip(ZipOutputStream zos, Map<String, String> fileMap, File baseDir, String zipSubDir) {
|
||||||
for (Map.Entry<String, String> entry : fileMap.entrySet()) {
|
for (Map.Entry<String, String> entry : fileMap.entrySet()) {
|
||||||
String relativePath = entry.getValue();
|
String relativePath = entry.getValue();
|
||||||
if (TextUtils.isEmpty(relativePath)) {
|
if (TextUtils.isEmpty(relativePath)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 拼接本地文件绝对路径
|
|
||||||
File localFile = new File(baseDir, relativePath);
|
File localFile = new File(baseDir, relativePath);
|
||||||
// 跳过不存在/非文件的路径
|
|
||||||
if (!localFile.exists() || !localFile.isFile()) {
|
if (!localFile.exists() || !localFile.isFile()) {
|
||||||
LogUtils.w(TAG, "跳过无效文件:" + localFile.getAbsolutePath());
|
LogUtils.w(TAG, "跳过无效文件:" + localFile.getAbsolutePath());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 拼接ZIP内的最终路径:子目录 + 原相对路径(如data/log/app.log)
|
|
||||||
String zipInnerPath = zipSubDir + relativePath;
|
String zipInnerPath = zipSubDir + relativePath;
|
||||||
// 将单个文件写入ZIP指定子目录
|
|
||||||
try {
|
try {
|
||||||
addSingleFileToZip(zos, localFile, zipInnerPath);
|
addSingleFileToZip(zos, localFile, zipInnerPath);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -354,23 +289,15 @@ public class BackupUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 将单个文件写入ZIP指定路径
|
|
||||||
* @param zos ZIP输出流
|
|
||||||
* @param localFile 本地待打包文件
|
|
||||||
* @param zipInnerPath ZIP内的最终路径(含子目录,如data/log/app.log)
|
|
||||||
*/
|
|
||||||
private void addSingleFileToZip(ZipOutputStream zos, File localFile, String zipInnerPath) throws IOException {
|
private void addSingleFileToZip(ZipOutputStream zos, File localFile, String zipInnerPath) throws IOException {
|
||||||
ZipEntry zipEntry = new ZipEntry(zipInnerPath);
|
ZipEntry zipEntry = new ZipEntry(zipInnerPath);
|
||||||
zos.putNextEntry(zipEntry);
|
zos.putNextEntry(zipEntry);
|
||||||
// 字节流读取文件,4096缓冲区适配Java7
|
|
||||||
FileInputStream fis = new FileInputStream(localFile);
|
FileInputStream fis = new FileInputStream(localFile);
|
||||||
byte[] buffer = new byte[4096];
|
byte[] buffer = new byte[4096];
|
||||||
int len;
|
int len;
|
||||||
while ((len = fis.read(buffer)) != -1) {
|
while ((len = fis.read(buffer)) != -1) {
|
||||||
zos.write(buffer, 0, len);
|
zos.write(buffer, 0, len);
|
||||||
}
|
}
|
||||||
// 关闭流和Entry,避免文件粘连/流泄漏
|
|
||||||
fis.close();
|
fis.close();
|
||||||
zos.closeEntry();
|
zos.closeEntry();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user