完成二次备份点击功能

This commit is contained in:
2026-01-31 18:52:01 +08:00
parent 55c653af09
commit 1db94b52e6
5 changed files with 158 additions and 161 deletions

View File

@@ -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

View File

@@ -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);
}
}
} }

View File

@@ -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

View File

@@ -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销毁已释放本地资源不影响二次备份初始化");
} }
} }

View File

@@ -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 + "/");
// 初始化双MapHashMap兼容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目录文件Mapnull/空则内部默认初始化
* @param sdcardFileMap 外部传入的SDCard目录文件Mapnull/空则内部默认初始化
* @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 待打包文件Mapkey=标识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();
} }