From 1db94b52e62654ca0a7278a044c0303cbb14c42d Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Sat, 31 Jan 2026 18:52:01 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BA=8C=E6=AC=A1=E5=A4=87?= =?UTF-8?q?=E4=BB=BD=E7=82=B9=E5=87=BB=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appbase/build.properties | 4 +- .../winboll/studio/appbase/MainActivity.java | 33 +++- libappbase/build.properties | 4 +- .../activities/FTPBackupsActivity.java | 141 ++++++++++++------ .../studio/libappbase/utils/BackupUtils.java | 137 ++++------------- 5 files changed, 158 insertions(+), 161 deletions(-) diff --git a/appbase/build.properties b/appbase/build.properties index bcd117a..7698d12 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -1,8 +1,8 @@ #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 libraryProject=libappbase baseVersion=15.15 publishVersion=15.15.11 -buildCount=17 +buildCount=20 baseBetaVersion=15.15.12 diff --git a/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java b/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java index 697953d..3517bd4 100644 --- a/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java +++ b/appbase/src/main/java/cc/winboll/studio/appbase/MainActivity.java @@ -14,6 +14,9 @@ import cc.winboll.studio.libappbase.LogActivity; import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.ToastUtils; 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 @@ -137,17 +140,35 @@ public class MainActivity extends Activity { // 启动意图(唤起浏览器) context.startActivity(intent); } - + public void onAboutActivity(View view) { LogUtils.d(TAG, "onAboutActivity() 调用"); Intent aboutIntent = new Intent(getApplicationContext(), AboutActivity.class); startActivity(aboutIntent); } - + public void onFTPBackupsActivity(View view) { - LogUtils.d(TAG, "onFTPBackupsActivity() 调用"); - Intent ftpBackupsIntent = new Intent(getApplicationContext(), FTPBackupsActivity.class); - startActivity(ftpBackupsIntent); - } + LogUtils.d(TAG, "onFTPBackupsActivity() 调用"); + SFTPBackupsSettingsDialog dlg = new SFTPBackupsSettingsDialog(this); + SFTPAuthModel authModel = dlg.getSFTPAuthModelFromSP(this); + if (authModel == null) { + dlg.show(); + } else { + // 1. 构建SDCard目录待备份文件Map(与BackupUtils的SdcardMap泛型一致:String-String) + HashMap 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); + } + } } diff --git a/libappbase/build.properties b/libappbase/build.properties index bcd117a..7698d12 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -1,8 +1,8 @@ #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 libraryProject=libappbase baseVersion=15.15 publishVersion=15.15.11 -buildCount=17 +buildCount=20 baseBetaVersion=15.15.12 diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/FTPBackupsActivity.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/FTPBackupsActivity.java index 72e5097..30d1bde 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/FTPBackupsActivity.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/activities/FTPBackupsActivity.java @@ -1,10 +1,16 @@ package cc.winboll.studio.libappbase.activities; import android.app.Activity; +import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Looper; 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.R; import cc.winboll.studio.libappbase.ToastUtils; @@ -15,8 +21,10 @@ import cc.winboll.studio.libappbase.dialogs.SFTPBackupsSettingsDialog; /** * BackupUtils 调用实例 + * 支持Intent传入双Map参数,初始化BackupUtils的待备份文件列表 * @Author 豆包&ZhanGSKen * @Date 2026/01/30 20:55 + * @LastEditTime 2026/02/01 04:00 */ public class FTPBackupsActivity extends Activity { @@ -26,72 +34,114 @@ public class FTPBackupsActivity extends Activity { // FTP服务器上传目标目录(可根据业务自定义) private static final String FTP_TARGET_DIR = "/WinBoLLStudio/APPBackups/WinBoLL/"; + // ==================== Intent传参常量(规范外部调用)==================== + /** + * Intent传入参数-Data目录待备份文件Map + * 类型:HashMap 实现Serializable接口 + */ + public static final String EXTRA_DATA_DIR_FILE_MAP = "extra_data_dir_file_map"; + /** + * Intent传入参数-SDCard目录待备份文件Map + * 类型:HashMap 实现Serializable接口 + */ + public static final String EXTRA_SDCARD_DIR_FILE_MAP = "extra_sdcard_dir_file_map"; + + // 解析后的双Map参数(用于初始化BackupUtils) + private Map mDataDirFileMap; + private Map mSdcardDirFileMap; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_ftp_backups); // 初始化主线程Handler,避免子线程更新UI崩溃 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) 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) 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); - dlg.show(); + dlg.show(); } /** * 点击事件:执行FTP备份(核心调用方法) * 主线程仅做UI触发,所有核心逻辑在子线程执行 + * 每次点击都重新解析Intent参数,保证获取最新的文件列表 */ public void onBackups(View view) { ToastUtils.show("开始执行FTP备份,请勿退出页面..."); LogUtils.d(TAG, "触发FTP备份操作,开启子线程执行核心逻辑"); - SFTPBackupsSettingsDialog dlg = new SFTPBackupsSettingsDialog(this); - SFTPAuthModel authModel = dlg.getSFTPAuthModelFromSP(this); - if (authModel == null) { - dlg.show(); - } else { - doBackups(authModel); - } + // 每次点击都重新解析Intent参数,避免Activity复用导致参数失效 + parseIntentParams(); + SFTPBackupsSettingsDialog dlg = new SFTPBackupsSettingsDialog(this); + SFTPAuthModel authModel = dlg.getSFTPAuthModelFromSP(this); + if (authModel == null) { + 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 dataDirFileMap, final Map sdcardDirFileMap) { // 所有BackupUtils操作放入子线程,规避网络/IO主线程异常 new Thread(new Runnable() { @Override public void run() { try { - // ================================= 步骤1:构建FTP登录配置 ================================= - - - // ================================= 步骤2:初始化BackupUtils单例 ================================= - // 推荐传Application上下文,这里临时传Activity(实际开发替换为getApplicationContext()) + // 初始化BackupUtils单例,透传解析后的双Map参数 BackupUtils backupUtils = BackupUtils.getInstance( - FTPBackupsActivity.this, + getApplicationContext(), authModel, - FTP_TARGET_DIR // FTP服务器上传目录 + FTP_TARGET_DIR, + dataDirFileMap, + sdcardDirFileMap ); - // ================================= 步骤3:添加待备份文件(Data目录+SDCard目录) ================================= - // 【应用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"); + LogUtils.d(TAG, "待备份文件初始化完成 → Data目录:" + backupUtils.getAllDataDirFiles().size() + "个 | SDCard目录:" + backupUtils.getAllSdcardFiles().size() + "个"); - // 【SDCard目录】添加文件:key=唯一标识,value=SDCard目录下的相对路径 - // 示例:/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登出,全程子线程执行 + // 核心执行:打包为ZIP + SFTP上传(内部已实现登录→打包→上传→登出) boolean isSuccess = backupUtils.packAndUploadByFtp(); - // ================================= 步骤5:主线程反馈执行结果 ================================= + // 主线程反馈执行结果 if (isSuccess) { updateUi("FTP备份成功!文件已打包为ZIP上传至服务器:" + FTP_TARGET_DIR); LogUtils.i(TAG, "FTP备份全流程执行成功"); @@ -109,18 +159,14 @@ public class FTPBackupsActivity extends Activity { LogUtils.e(TAG, "FTP备份执行异常:" + e.getMessage(), e); updateUi("备份执行失败:" + e.getMessage()); } finally { - // 兜底:清空BackupUtils的文件列表(避免重复添加) - if (BackupUtils.getInstance() != null) { - BackupUtils.getInstance().clearDataDirFiles(); - BackupUtils.getInstance().clearSdcardFiles(); - LogUtils.d(TAG, "兜底清空待备份文件列表"); - } - // 兜底:断开FTP连接,释放所有资源 - FTPUtils.getInstance().logout(); + // 仅保留FTP断连,BackupUtils内部已兜底登出/断连,此处做双重保障 + FTPUtils.getInstance().disconnect(); + // 关键日志:明确仅删除临时ZIP包,不修改/删除源文件 + LogUtils.d(TAG, "备份流程结束:仅删除本地临时ZIP打包文件,待备份源文件未做任何修改/删除"); } } }).start(); - } + } /** * 子线程更新UI工具方法(Toast提示) @@ -137,6 +183,7 @@ public class FTPBackupsActivity extends Activity { /** * 页面销毁:释放资源,避免内存泄漏 + * 仅释放本地资源,不清空Map/重复断连FTP */ @Override protected void onDestroy() { @@ -145,8 +192,10 @@ public class FTPBackupsActivity extends Activity { if (mMainHandler != null) { mMainHandler.removeCallbacksAndMessages(null); } - // 断开FTP连接,释放BackupUtils相关资源 - FTPUtils.getInstance().logout(); + // 清空本地Map引用,协助GC(不影响BackupUtils内部逻辑) + mDataDirFileMap = null; + mSdcardDirFileMap = null; + LogUtils.d(TAG, "FTPBackupsActivity销毁:已释放本地资源,不影响二次备份初始化"); } } diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/BackupUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/BackupUtils.java index d169085..7530abb 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/BackupUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/BackupUtils.java @@ -26,7 +26,7 @@ import cc.winboll.studio.libappbase.models.SFTPAuthModel; * 兼容:Java7、Android 6.0+,无第三方依赖(ZIP为原生实现),免动态读写权限 * @Author 豆包&ZhanGSKen * @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 static final String TAG = "BackupUtils"; @@ -50,29 +50,41 @@ public class BackupUtils { // 应用专属外部文件目录(SDCard Map的基础根目录,初始化时赋值,避免重复创建) private File mAppExternalFilesDir; - // 私有构造器:禁止外部实例化,初始化双Map+配置参数+标准化SFTP上传目录 - private BackupUtils(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir) { + // 私有构造器:新增双Map入参,空值则使用内部默认初始化,非空则用入参初始化 + private BackupUtils(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir, + Map dataDirFileMap, Map sdcardFileMap) { this.mAppContext = context.getApplicationContext(); this.mFtpAuthModel = ftpAuthModel; // 初始化SDCard Map的基础根目录:应用专属外部文件目录(/storage/emulated/0/Android/data/[包名]/files) this.mAppExternalFilesDir = mAppContext.getExternalFilesDir(null); // 标准化SFTP上传目录:空则默认/,非空则补全结尾斜杠 this.mFtpTargetDir = TextUtils.isEmpty(ftpTargetDir) ? "/" : (ftpTargetDir.endsWith("/") ? ftpTargetDir : ftpTargetDir + "/"); - // 初始化双Map(HashMap兼容Java7) - mDataDirFileMap = new HashMap<>(); - mSdcardFileMap = new HashMap<>(); + + // 核心修改:入参Map非空且非空集合时,使用入参初始化;否则内部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, "SDCard Map基础根目录:" + (mAppExternalFilesDir == null ? "获取失败" : mAppExternalFilesDir.getAbsolutePath())); + LogUtils.d(TAG, "初始化后DataMap大小:" + mDataDirFileMap.size() + " | SdcardMap大小:" + mSdcardFileMap.size()); } /** * 单例初始化方法(必须先调用,否则getInstance()会抛异常) + * 新增双Map入参,支持外部初始化待备份文件列表 * @param context 上下文(推荐传Application,避免内存泄漏) * @param ftpAuthModel 外部SFTP认证实体类(含服务器/账号/端口等) * @param ftpTargetDir SFTP服务器指定上传目录(如/backup,自动补全斜杠) + * @param dataDirFileMap 外部传入的Data目录文件Map,null/空则内部默认初始化 + * @param sdcardFileMap 外部传入的SDCard目录文件Map,null/空则内部默认初始化 * @return BackupUtils单例实例 */ - public static BackupUtils getInstance(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir) { + public static BackupUtils getInstance(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir, + Map dataDirFileMap, Map sdcardFileMap) { if (sInstance == null) { synchronized (BackupUtils.class) { if (sInstance == null) { @@ -83,30 +95,34 @@ public class BackupUtils { if (ftpAuthModel == null || TextUtils.isEmpty(ftpAuthModel.getFtpServer())) { throw new IllegalArgumentException("初始化失败:SFTPAuthModel/ftpServer 不能为空"); } - sInstance = new BackupUtils(context, ftpAuthModel, ftpTargetDir); + // 透传新增的双Map入参至构造器 + sInstance = new BackupUtils(context, ftpAuthModel, ftpTargetDir, dataDirFileMap, sdcardFileMap); } } } return sInstance; } + /** + * 重载默认初始化方法:兼容原有调用逻辑,无需传入Map,内部默认初始化 + * 避免修改后影响原有代码调用 + */ + public static BackupUtils getInstance(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir) { + return getInstance(context, ftpAuthModel, ftpTargetDir, null, null); + } + /** * 获取单例实例(需先调用带参getInstance初始化) * @return BackupUtils单例实例 */ public static BackupUtils getInstance() { if (sInstance == null) { - throw new IllegalStateException("BackupUtils未初始化,请先调用getInstance(Context, SFTPAuthModel, String)"); + throw new IllegalStateException("BackupUtils未初始化,请先调用getInstance(Context, SFTPAuthModel, String[, Map, Map])"); } return sInstance; } - // ====================================== 应用Data目录 - Map操作方法 ====================================== - /** - * 添加应用Data目录下的备份文件(相对路径) - * @param key 文件唯一标识(如log_20260130,避免重复) - * @param relativePath Data目录下的相对路径,如:log/app.log - */ + // ====================================== 以下原有方法均未修改 ====================================== public void addDataDirFile(String key, String relativePath) { if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(relativePath)) { mDataDirFileMap.put(key, relativePath); @@ -114,10 +130,6 @@ public class BackupUtils { } } - /** - * 移除应用Data目录下的指定备份文件 - * @param key 文件唯一标识 - */ public void removeDataDirFile(String key) { if (!TextUtils.isEmpty(key) && mDataDirFileMap.containsKey(key)) { mDataDirFileMap.remove(key); @@ -125,37 +137,19 @@ public class BackupUtils { } } - /** - * 获取应用Data目录下指定标识的文件相对路径 - * @param key 文件唯一标识 - * @return 相对路径,无则返回null - */ public String getDataDirFile(String key) { return mDataDirFileMap.get(key); } - /** - * 获取Data目录下所有备份文件(返回新Map,防止外部篡改原数据) - * @return 只读Map副本 - */ public Map getAllDataDirFiles() { return new HashMap<>(mDataDirFileMap); } - /** - * 清空应用Data目录下的所有备份文件 - */ public void clearDataDirFiles() { mDataDirFileMap.clear(); LogUtils.d(TAG, "清空Data目录所有备份文件"); } - // ====================================== 应用专属外部文件目录 - Map操作方法 ====================================== - /** - * 添加应用专属外部文件目录下的备份文件(相对路径) - * @param key 文件唯一标识(如crash_20260130,避免重复) - * @param relativePath 外部文件目录下的相对路径,如:backup/crash.log - */ public void addSdcardFile(String key, String relativePath) { if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(relativePath) && mAppExternalFilesDir != null) { mSdcardFileMap.put(key, relativePath); @@ -163,10 +157,6 @@ public class BackupUtils { } } - /** - * 移除应用专属外部文件目录下的指定备份文件 - * @param key 文件唯一标识 - */ public void removeSdcardFile(String key) { if (!TextUtils.isEmpty(key) && mSdcardFileMap.containsKey(key)) { mSdcardFileMap.remove(key); @@ -174,41 +164,20 @@ public class BackupUtils { } } - /** - * 获取应用专属外部文件目录下指定标识的文件相对路径 - * @param key 文件唯一标识 - * @return 相对路径,无则返回null - */ public String getSdcardFile(String key) { return mSdcardFileMap.get(key); } - /** - * 获取应用专属外部文件目录下所有备份文件(返回新Map,防止外部篡改原数据) - * @return 只读Map副本 - */ public Map getAllSdcardFiles() { return new HashMap<>(mSdcardFileMap); } - /** - * 清空应用专属外部文件目录下的所有备份文件 - */ public void clearSdcardFiles() { mSdcardFileMap.clear(); 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() { - // 前置校验:无待备份文件/外部文件目录获取失败,直接返回失败 if (mDataDirFileMap.isEmpty() && mSdcardFileMap.isEmpty()) { LogUtils.e(TAG, "SFTP上传失败:无待备份文件(DataDir+外部文件目录均为空)"); return false; @@ -218,21 +187,15 @@ public class BackupUtils { return false; } - // 1. 生成ZIP文件名(UUID去横杠 + 毫秒时间戳,保证全球唯一) String zipFileName = UUID.randomUUID().toString().replace("-", "") + "-" + System.currentTimeMillis() + ".zip"; - // 本地临时ZIP文件:应用外部缓存目录(Android 6.0+免读写权限) File tempZipFile = new File(mAppContext.getExternalCacheDir(), zipFileName); - // SFTP远程完整路径:标准化上传目录 + ZIP文件名(已提前补全斜杠,直接拼接) String remoteFtpFilePath = mFtpTargetDir + zipFileName; - // 2. 获取FTPUtils单例(全局唯一) FTPUtils ftpUtils = FTPUtils.getInstance(); - // 上传结果标记 boolean isUploadSuccess = false; try { - // ==================== 第一步:SFTP服务器登录(基于外部SFTPAuthModel)==================== LogUtils.d(TAG, "开始SFTP登录:" + mFtpAuthModel.getFtpServer() + ":" + mFtpAuthModel.getFtpPort()); boolean isFtpLogin = ftpUtils.login(mFtpAuthModel); if (!isFtpLogin) { @@ -241,7 +204,6 @@ public class BackupUtils { } LogUtils.i(TAG, "SFTP登录成功,准备打包文件:" + zipFileName); - // ==================== 第二步:本地打包双Map文件为ZIP(分data/sdcard目录,Java7原生实现)==================== LogUtils.d(TAG, "开始本地ZIP打包(分data/sdcard目录),临时文件路径:" + tempZipFile.getAbsolutePath()); boolean isPackSuccess = packFilesToZip(tempZipFile); if (!isPackSuccess || !tempZipFile.exists() || tempZipFile.length() == 0) { @@ -250,7 +212,6 @@ public class BackupUtils { } LogUtils.i(TAG, "ZIP打包成功,文件大小:" + tempZipFile.length() / 1024 + "KB"); - // ==================== 第三步:SFTP文件上传(调用现有FTPUtils.uploadFile)==================== LogUtils.d(TAG, "开始SFTP上传:本地→SFTP" + remoteFtpFilePath); isUploadSuccess = ftpUtils.uploadFile(tempZipFile.getAbsolutePath(), remoteFtpFilePath); if (isUploadSuccess) { @@ -260,47 +221,33 @@ public class BackupUtils { } } catch (Exception e) { - // 捕获所有运行时异常,避免崩溃 LogUtils.e(TAG, "SFTP上传异常:" + e.getMessage(), e); isUploadSuccess = false; } finally { - // ==================== 最终兜底:无论成功/失败,释放所有资源 ==================== - // 1. SFTP登出+断开连接(避免服务端连接数耗尽) if (ftpUtils.isConnected()) { ftpUtils.logout(); } ftpUtils.disconnect(); - // 2. 删除本地临时ZIP文件(释放存储空间,避免缓存堆积) if (tempZipFile.exists()) { boolean isDelete = tempZipFile.delete(); LogUtils.d(TAG, "本地临时ZIP文件删除:" + (isDelete ? "成功" : "失败")); } - // 3. 清空临时变量,协助GC System.gc(); } return isUploadSuccess; } - // ====================================== 私有工具方法:ZIP打包/目录校验 ====================================== - /** - * Java7原生ZIP打包:分目录打包(data/sdcard),遍历双Map文件写入ZIP - * @param zipFile 生成的临时ZIP文件 - * @return true=打包成功,false=打包失败 - */ private boolean packFilesToZip(File zipFile) { ZipOutputStream zos = null; try { - // 初始化ZIP输出流,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()) { packDirFilesToZip(zos, mDataDirFileMap, mAppContext.getFilesDir(), ZIP_DIR_DATA); LogUtils.d(TAG, "Data目录文件已打包到ZIP→" + ZIP_DIR_DATA + "子目录"); } - // 2. 打包应用专属外部文件目录下的文件 → ZIP内的sdcard子目录 if (!mSdcardFileMap.isEmpty() && mAppExternalFilesDir != null) { packDirFilesToZip(zos, mSdcardFileMap, mAppExternalFilesDir, 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); return false; } finally { - // 关闭ZIP流,释放资源 if (zos != null) { try { 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 fileMap, File baseDir, String zipSubDir) { for (Map.Entry entry : fileMap.entrySet()) { String relativePath = entry.getValue(); if (TextUtils.isEmpty(relativePath)) { continue; } - // 拼接本地文件绝对路径 File localFile = new File(baseDir, relativePath); - // 跳过不存在/非文件的路径 if (!localFile.exists() || !localFile.isFile()) { LogUtils.w(TAG, "跳过无效文件:" + localFile.getAbsolutePath()); continue; } - // 拼接ZIP内的最终路径:子目录 + 原相对路径(如data/log/app.log) String zipInnerPath = zipSubDir + relativePath; - // 将单个文件写入ZIP指定子目录 try { addSingleFileToZip(zos, localFile, zipInnerPath); } 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 { ZipEntry zipEntry = new ZipEntry(zipInnerPath); zos.putNextEntry(zipEntry); - // 字节流读取文件,4096缓冲区适配Java7 FileInputStream fis = new FileInputStream(localFile); byte[] buffer = new byte[4096]; int len; while ((len = fis.read(buffer)) != -1) { zos.write(buffer, 0, len); } - // 关闭流和Entry,避免文件粘连/流泄漏 fis.close(); zos.closeEntry(); }