From 55c653af099dc9046503c27db34c2148f9385265 Mon Sep 17 00:00:00 2001 From: ZhanGSKen Date: Sat, 31 Jan 2026 14:18:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=A4=87=E4=BB=BD=E6=89=93?= =?UTF-8?q?=E5=8C=85=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appbase/build.properties | 4 +- libappbase/build.properties | 4 +- .../activities/FTPBackupsActivity.java | 137 ++++++- .../dialogs/SFTPBackupsSettingsDialog.java | 372 ++++++++++++++++++ .../{FTPAuthModel.java => SFTPAuthModel.java} | 44 +-- .../studio/libappbase/utils/BackupUtils.java | 163 ++++---- .../studio/libappbase/utils/FTPUtils.java | 6 +- .../main/res/drawable/shape_edittext_bg.xml | 18 + .../main/res/layout/activity_ftp_backups.xml | 11 +- .../layout/dialog_sftp_backup_settings.xml | 163 ++++++++ 10 files changed, 803 insertions(+), 119 deletions(-) create mode 100644 libappbase/src/main/java/cc/winboll/studio/libappbase/dialogs/SFTPBackupsSettingsDialog.java rename libappbase/src/main/java/cc/winboll/studio/libappbase/models/{FTPAuthModel.java => SFTPAuthModel.java} (57%) create mode 100644 libappbase/src/main/res/drawable/shape_edittext_bg.xml create mode 100644 libappbase/src/main/res/layout/dialog_sftp_backup_settings.xml diff --git a/appbase/build.properties b/appbase/build.properties index 62d6386..bcd117a 100644 --- a/appbase/build.properties +++ b/appbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Fri Jan 30 13:37:25 GMT 2026 +#Sat Jan 31 06:16:42 GMT 2026 stageCount=12 libraryProject=libappbase baseVersion=15.15 publishVersion=15.15.11 -buildCount=1 +buildCount=17 baseBetaVersion=15.15.12 diff --git a/libappbase/build.properties b/libappbase/build.properties index 62d6386..bcd117a 100644 --- a/libappbase/build.properties +++ b/libappbase/build.properties @@ -1,8 +1,8 @@ #Created by .winboll/winboll_app_build.gradle -#Fri Jan 30 13:37:25 GMT 2026 +#Sat Jan 31 06:16:42 GMT 2026 stageCount=12 libraryProject=libappbase baseVersion=15.15 publishVersion=15.15.11 -buildCount=1 +buildCount=17 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 86bc506..72e5097 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 @@ -2,26 +2,151 @@ package cc.winboll.studio.libappbase.activities; import android.app.Activity; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.View; +import cc.winboll.studio.libappbase.LogUtils; import cc.winboll.studio.libappbase.R; import cc.winboll.studio.libappbase.ToastUtils; +import cc.winboll.studio.libappbase.models.SFTPAuthModel; +import cc.winboll.studio.libappbase.utils.BackupUtils; +import cc.winboll.studio.libappbase.utils.FTPUtils; +import cc.winboll.studio.libappbase.dialogs.SFTPBackupsSettingsDialog; /** + * BackupUtils 调用实例 * @Author 豆包&ZhanGSKen * @Date 2026/01/30 20:55 */ public class FTPBackupsActivity extends Activity { - + public static final String TAG = "FTPBackupsActivity"; - + // 主线程Handler:子线程更新UI专用 + private Handler mMainHandler; + // FTP服务器上传目标目录(可根据业务自定义) + private static final String FTP_TARGET_DIR = "/WinBoLLStudio/APPBackups/WinBoLL/"; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_ftp_backups); - + // 初始化主线程Handler,避免子线程更新UI崩溃 + mMainHandler = new Handler(Looper.getMainLooper()); } - - public void onBackups(View view) { - ToastUtils.show("onBackups"); + + public void onSFTPSettings(View view) { + SFTPBackupsSettingsDialog dlg = new SFTPBackupsSettingsDialog(this); + dlg.show(); + } + + /** + * 点击事件:执行FTP备份(核心调用方法) + * 主线程仅做UI触发,所有核心逻辑在子线程执行 + */ + 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); + } + } + + void doBackups(final SFTPAuthModel authModel) { + + // 所有BackupUtils操作放入子线程,规避网络/IO主线程异常 + new Thread(new Runnable() { + @Override + public void run() { + try { + // ================================= 步骤1:构建FTP登录配置 ================================= + + + // ================================= 步骤2:初始化BackupUtils单例 ================================= + // 推荐传Application上下文,这里临时传Activity(实际开发替换为getApplicationContext()) + BackupUtils backupUtils = BackupUtils.getInstance( + FTPBackupsActivity.this, + authModel, + FTP_TARGET_DIR // FTP服务器上传目录 + ); + + // ================================= 步骤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"); + + // 【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登出,全程子线程执行 + boolean isSuccess = backupUtils.packAndUploadByFtp(); + + // ================================= 步骤5:主线程反馈执行结果 ================================= + if (isSuccess) { + updateUi("FTP备份成功!文件已打包为ZIP上传至服务器:" + FTP_TARGET_DIR); + LogUtils.i(TAG, "FTP备份全流程执行成功"); + } else { + updateUi("FTP备份失败!请检查服务器配置或文件路径"); + LogUtils.e(TAG, "FTP备份全流程执行失败"); + } + + } catch (IllegalArgumentException e) { + // 捕获初始化参数异常 + LogUtils.e(TAG, "BackupUtils初始化失败:" + e.getMessage(), e); + updateUi("备份初始化失败:" + e.getMessage()); + } catch (Exception e) { + // 捕获所有执行异常 + 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(); + } + } + }).start(); } + + /** + * 子线程更新UI工具方法(Toast提示) + * @param msg 提示信息 + */ + private void updateUi(final String msg) { + mMainHandler.post(new Runnable() { + @Override + public void run() { + ToastUtils.show(msg); + } + }); + } + + /** + * 页面销毁:释放资源,避免内存泄漏 + */ + @Override + protected void onDestroy() { + super.onDestroy(); + // 移除Handler未执行的任务 + if (mMainHandler != null) { + mMainHandler.removeCallbacksAndMessages(null); + } + // 断开FTP连接,释放BackupUtils相关资源 + FTPUtils.getInstance().logout(); + } } + diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/dialogs/SFTPBackupsSettingsDialog.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/dialogs/SFTPBackupsSettingsDialog.java new file mode 100644 index 0000000..e7dd73c --- /dev/null +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/dialogs/SFTPBackupsSettingsDialog.java @@ -0,0 +1,372 @@ +package cc.winboll.studio.libappbase.dialogs; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.SharedPreferences; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.Toast; + +import cc.winboll.studio.libappbase.LogUtils; +import cc.winboll.studio.libappbase.R; +import cc.winboll.studio.libappbase.models.SFTPAuthModel; + +/** + * SFTP备份设置对话框 + * 功能:1.布局式UI展示SFTP配置项 2.SP持久化存储SFTPAuthModel模型数据 3.提供公开静态方法供外部读取/清空SP配置 + * 存储模式:SharedPreferences私有模式,仅本应用可访问 + * UI实现:通过XML布局文件自定义控件,支持账号密码/秘钥双登录方式配置 + * @Author 豆包&ZhanGSKen + * @Date 2026/01/31 12:36:00 + * @LastEditTime 2026/01/31 21:58:00 + */ +public class SFTPBackupsSettingsDialog { + // ************************ 常量属性 ************************ + public static final String TAG = "SFTPBackupsSettingsDialog"; + // SP存储核心常量 + private static final String SP_NAME = "sftp_backup_settings_sp"; + private static final String SP_KEY_PREFIX = "sftp_backup_"; + // SP各字段存储Key + private static final String KEY_SERVER = SP_KEY_PREFIX + "server"; + private static final String KEY_PORT = SP_KEY_PREFIX + "port"; + private static final String KEY_USERNAME = SP_KEY_PREFIX + "username"; + private static final String KEY_PASSWORD = SP_KEY_PREFIX + "password"; + private static final String KEY_KEY_PATH = SP_KEY_PREFIX + "key_path"; + private static final String KEY_KEY_PWD = SP_KEY_PREFIX + "key_pwd"; + private static final String KEY_ACTIVE_MODE = SP_KEY_PREFIX + "active_mode"; + private static final String KEY_CHARSET = SP_KEY_PREFIX + "charset"; + // SFTP默认配置常量(抽离便于维护) + private static final int SFTP_DEFAULT_PORT = 22; + private static final String SFTP_DEFAULT_CHARSET = "UTF-8"; + + // ************************ 成员属性 ************************ + // 上下文与对话框核心实例 + private final Context mContext; + private AlertDialog mDialog; + // 布局控件实例(按配置模块排序,便于查找) + private EditText etServer; + private EditText etPort; + private EditText etUsername; + private EditText etPwd; + private EditText etKeyPath; + private EditText etKeyPwd; + private EditText etCharset; + private CheckBox cbActiveMode; + private Button btnSave; + private Button btnCancel; + private Button btnClear; + + // ************************ 构造方法 ************************ + /** + * 构造方法:初始化对话框核心依赖与流程 + * @param context 上下文(推荐Application/Activity,避免内存泄漏) + */ + public SFTPBackupsSettingsDialog(Context context) { + LogUtils.d(TAG, "SFTPBackupsSettingsDialog构造方法调用,传入上下文:" + context.getClass().getSimpleName()); + this.mContext = context; + initDialog(); + initView(); + initData(); + initListener(); + LogUtils.d(TAG, "SFTPBackupsSettingsDialog初始化全流程执行完成"); + } + + // ************************ 公共方法 ************************ + /** + * 显示SFTP配置对话框(做非空/非显示校验,避免重复显示) + */ + public void show() { + LogUtils.d(TAG, "show()方法调用,对话框当前状态:" + (mDialog == null ? "未初始化" : mDialog.isShowing() ? "已显示" : "未显示")); + if (mDialog != null && !mDialog.isShowing()) { + mDialog.show(); + LogUtils.i(TAG, "SFTP配置对话框显示成功"); + } + } + + /** + * 【公开静态方法】从SP读取SFTPAuthModel配置模型 + * 外部可直接调用,无有效配置(服务器地址空)返回null + * @param context 上下文 + * @return 已存储的配置模型/无有效配置返回null + */ + public static SFTPAuthModel getSFTPAuthModelFromSP(Context context) { + LogUtils.d(TAG, "getSFTPAuthModelFromSP()静态方法调用,传入上下文:" + (context == null ? "null" : context.getClass().getSimpleName())); + try { + // 前置非空校验,避免空指针 + if (context == null) { + LogUtils.e(TAG, "getSFTPAuthModelFromSP()执行失败:传入上下文为null"); + return null; + } + SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); + SFTPAuthModel model = new SFTPAuthModel(); + // 读取基础配置 + model.setFtpServer(sp.getString(KEY_SERVER, null)); + model.setFtpPort(sp.getInt(KEY_PORT, SFTP_DEFAULT_PORT)); + model.setFtpUsername(sp.getString(KEY_USERNAME, null)); + model.setFtpPassword(sp.getString(KEY_PASSWORD, null)); + // 读取秘钥配置 + model.setFtpKeyPath(sp.getString(KEY_KEY_PATH, null)); + model.setFtpKeyPwd(sp.getString(KEY_KEY_PWD, null)); + // 读取高级配置 + model.setFtpCharset(sp.getString(KEY_CHARSET, SFTP_DEFAULT_CHARSET)); + + // 核心有效校验:服务器地址为空则判定为无有效配置 + if (TextUtils.isEmpty(model.getFtpServer())) { + LogUtils.w(TAG, "getSFTPAuthModelFromSP()执行完成:SP中无有效SFTP配置(服务器地址为空)"); + return null; + } + LogUtils.i(TAG, "getSFTPAuthModelFromSP()执行成功,读取到SFTP配置:" + model.getFtpServer() + ":" + model.getFtpPort()); + return model; + } catch (Exception e) { + LogUtils.e(TAG, "getSFTPAuthModelFromSP()执行异常:" + e.getMessage(), e); + return null; + } + } + + /** + * 【公开静态方法】清空SP中所有SFTP配置数据 + * 外部可直接调用,做前置非空校验避免崩溃 + * @param context 上下文 + */ + public static void clearSFTPAuthModelFromSP(Context context) { + LogUtils.d(TAG, "clearSFTPAuthModelFromSP()静态方法调用,传入上下文:" + (context == null ? "null" : context.getClass().getSimpleName())); + try { + if (context == null) { + LogUtils.e(TAG, "clearSFTPAuthModelFromSP()执行失败:传入上下文为null"); + return; + } + SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + editor.clear(); + editor.commit(); + LogUtils.i(TAG, "clearSFTPAuthModelFromSP()执行成功:SP中SFTP配置已全部清空"); + } catch (Exception e) { + LogUtils.e(TAG, "clearSFTPAuthModelFromSP()执行异常:" + e.getMessage(), e); + } + } + + // ************************ 私有初始化方法 ************************ + /** + * 初始化对话框基础样式与布局绑定 + */ + private void initDialog() { + LogUtils.d(TAG, "initDialog()方法调用,开始初始化对话框基础样式"); + View dialogView = LayoutInflater.from(mContext).inflate(R.layout.dialog_sftp_backup_settings, null); + AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + builder.setView(dialogView); + builder.setCancelable(false); // 禁止外部点击/返回键关闭 + mDialog = builder.create(); + // 对话框样式适配,解决全屏/背景异常问题 + if (mDialog.getWindow() != null) { + mDialog.getWindow().setBackgroundDrawableResource(android.R.color.white); + } + LogUtils.d(TAG, "initDialog()执行完成,对话框基础实例创建成功"); + } + + /** + * 初始化布局控件:绑定XML控件ID与Java实例(一一对应) + */ + private void initView() { + LogUtils.d(TAG, "initView()方法调用,开始绑定布局控件实例"); + View rootView = mDialog.getLayoutInflater().inflate(R.layout.dialog_sftp_backup_settings, null); + // 基础配置控件 + etServer = rootView.findViewById(R.id.et_sftp_server); + etPort = rootView.findViewById(R.id.et_sftp_port); + etUsername = rootView.findViewById(R.id.et_sftp_username); + etPwd = rootView.findViewById(R.id.et_sftp_pwd); + // 秘钥配置控件 + etKeyPath = rootView.findViewById(R.id.et_sftp_key_path); + etKeyPwd = rootView.findViewById(R.id.et_sftp_key_pwd); + // 高级配置控件 + etCharset = rootView.findViewById(R.id.et_sftp_charset); + // 操作按钮控件 + btnSave = rootView.findViewById(R.id.btn_sftp_save); + btnCancel = rootView.findViewById(R.id.btn_sftp_cancel); + btnClear = rootView.findViewById(R.id.btn_sftp_clear); + // 重新绑定对话框View,确保控件生效 + mDialog.setView(rootView); + LogUtils.d(TAG, "initView()执行完成,所有布局控件绑定成功"); + } + + /** + * 初始化控件数据:从SP读取配置并回显到控件,无配置则设默认值 + */ + private void initData() { + LogUtils.d(TAG, "initData()方法调用,开始回显SP配置到控件"); + SFTPAuthModel model = getSFTPAuthModelFromSP(mContext); + if (model == null) { + LogUtils.w(TAG, "initData()执行提示:SP中无有效SFTP配置,控件设置默认值"); + // 无配置时设置默认值,提升用户体验 + etPort.setText(String.valueOf(SFTP_DEFAULT_PORT)); + etCharset.setText(SFTP_DEFAULT_CHARSET); + return; + } + // 回显基础配置(做非空校验,避免空指针) + etServer.setText(TextUtils.isEmpty(model.getFtpServer()) ? "" : model.getFtpServer()); + etPort.setText(model.getFtpPort() <= 0 ? String.valueOf(SFTP_DEFAULT_PORT) : String.valueOf(model.getFtpPort())); + etUsername.setText(TextUtils.isEmpty(model.getFtpUsername()) ? "" : model.getFtpUsername()); + etPwd.setText(TextUtils.isEmpty(model.getFtpPassword()) ? "" : model.getFtpPassword()); + // 回显秘钥配置 + etKeyPath.setText(TextUtils.isEmpty(model.getFtpKeyPath()) ? "" : model.getFtpKeyPath()); + etKeyPwd.setText(TextUtils.isEmpty(model.getFtpKeyPwd()) ? "" : model.getFtpKeyPwd()); + // 回显高级配置 + etCharset.setText(TextUtils.isEmpty(model.getFtpCharset()) ? SFTP_DEFAULT_CHARSET : model.getFtpCharset()); + LogUtils.d(TAG, "initData()执行完成,SP配置已成功回显到所有控件"); + } + + /** + * 初始化控件监听:为所有按钮设置Java7原生点击事件(匿名内部类) + */ + private void initListener() { + LogUtils.d(TAG, "initListener()方法调用,开始设置控件点击事件"); + // 保存按钮点击事件 + btnSave.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onSaveClick(); + } + }); + // 取消按钮点击事件 + btnCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onCancelClick(); + } + }); + // 清空按钮点击事件 + btnClear.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onClearClick(); + } + }); + LogUtils.d(TAG, "initListener()执行完成,所有按钮点击事件设置成功"); + } + + // ************************ 私有事件处理方法 ************************ + /** + * 保存按钮点击事件处理:控件数据→模型封装→有效性校验→SP持久化 + */ + private void onSaveClick() { + LogUtils.d(TAG, "onSaveClick()方法调用,开始处理配置保存逻辑"); + SFTPAuthModel model = getModelFromView(); + if (checkModelValid(model)) { + saveSFTPAuthModelToSP(mContext, model); + Toast.makeText(mContext, "SFTP配置保存成功", Toast.LENGTH_SHORT).show(); + mDialog.dismiss(); + LogUtils.i(TAG, "SFTP配置保存成功,服务器配置:" + model.getFtpServer() + ":" + model.getFtpPort()); + } + } + + /** + * 取消按钮点击事件处理:直接关闭对话框,不做任何数据修改 + */ + private void onCancelClick() { + LogUtils.d(TAG, "onCancelClick()方法调用,开始处理取消配置逻辑"); + mDialog.dismiss(); + LogUtils.i(TAG, "SFTP配置对话框已关闭(用户主动取消)"); + } + + /** + * 清空按钮点击事件处理:清空所有控件输入数据,恢复默认值(不修改SP) + */ + private void onClearClick() { + LogUtils.d(TAG, "onClearClick()方法调用,开始处理控件数据清空逻辑"); + // 清空基础配置控件 + etServer.setText(""); + etPort.setText(String.valueOf(SFTP_DEFAULT_PORT)); + etUsername.setText(""); + etPwd.setText(""); + // 清空秘钥配置控件 + etKeyPath.setText(""); + etKeyPwd.setText(""); + // 清空高级配置控件 + cbActiveMode.setChecked(false); + etCharset.setText(SFTP_DEFAULT_CHARSET); + // 提示用户 + Toast.makeText(mContext, "控件数据已清空", Toast.LENGTH_SHORT).show(); + LogUtils.d(TAG, "onClearClick()执行完成,所有控件数据已恢复默认值"); + } + + // ************************ 私有业务方法 ************************ + /** + * 从控件读取输入数据,封装为SFTPAuthModel模型 + * 做基础非空处理,避免空字符串存入SP + * @return 封装后的SFTP配置模型 + */ + private SFTPAuthModel getModelFromView() { + LogUtils.d(TAG, "getModelFromView()方法调用,开始从控件读取数据封装模型"); + SFTPAuthModel model = new SFTPAuthModel(); + // 读取基础配置(trim去空格,避免无效空格) + model.setFtpServer(etServer.getText().toString().trim()); + String portStr = etPort.getText().toString().trim(); + model.setFtpPort(TextUtils.isEmpty(portStr) ? SFTP_DEFAULT_PORT : Integer.parseInt(portStr)); + model.setFtpUsername(etUsername.getText().toString().trim()); + model.setFtpPassword(etPwd.getText().toString().trim()); + // 读取秘钥配置 + model.setFtpKeyPath(etKeyPath.getText().toString().trim()); + model.setFtpKeyPwd(etKeyPwd.getText().toString().trim()); + // 读取高级配置 + model.setFtpCharset(TextUtils.isEmpty(etCharset.getText().toString().trim()) ? SFTP_DEFAULT_CHARSET : etCharset.getText().toString().trim()); + LogUtils.d(TAG, "getModelFromView()执行完成,控件数据已成功封装为SFTPAuthModel"); + return model; + } + + /** + * 校验SFTP配置模型的有效性(核心必选参数校验) + * 仅校验服务器地址和端口号,其他参数为可选 + * @param model 待校验的SFTP配置模型 + * @return true=配置有效 false=配置无效 + */ + private boolean checkModelValid(SFTPAuthModel model) { + LogUtils.d(TAG, "checkModelValid()方法调用,开始校验配置模型有效性"); + // 校验服务器地址(必选) + if (TextUtils.isEmpty(model.getFtpServer())) { + Toast.makeText(mContext, "请输入SFTP服务器地址", Toast.LENGTH_SHORT).show(); + LogUtils.w(TAG, "配置校验失败:SFTP服务器地址为空"); + return false; + } + // 校验端口号(必选,1-65535合法范围) + if (model.getFtpPort() <= 0 || model.getFtpPort() > 65535) { + Toast.makeText(mContext, "请输入有效的端口号(1-65535)", Toast.LENGTH_SHORT).show(); + LogUtils.w(TAG, "配置校验失败:SFTP端口号无效,当前值:" + model.getFtpPort()); + return false; + } + LogUtils.d(TAG, "checkModelValid()执行完成,SFTP配置模型核心参数校验通过"); + return true; + } + + /** + * 将SFTPAuthModel模型数据存入SP(私有方法,仅内部调用) + * 使用commit同步提交,保证配置即时生效 + * @param context 上下文 + * @param model 待存储的SFTP配置模型 + */ + private static void saveSFTPAuthModelToSP(Context context, SFTPAuthModel model) { + LogUtils.d(TAG, "saveSFTPAuthModelToSP()方法调用,准备存储SFTP配置:" + model.getFtpServer() + ":" + model.getFtpPort()); + try { + SharedPreferences sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sp.edit(); + // 存储基础配置 + editor.putString(KEY_SERVER, model.getFtpServer()); + editor.putInt(KEY_PORT, model.getFtpPort()); + editor.putString(KEY_USERNAME, model.getFtpUsername()); + editor.putString(KEY_PASSWORD, model.getFtpPassword()); + // 存储秘钥配置 + editor.putString(KEY_KEY_PATH, model.getFtpKeyPath()); + editor.putString(KEY_KEY_PWD, model.getFtpKeyPwd()); + // 存储高级配置 + editor.putString(KEY_CHARSET, model.getFtpCharset()); + // 同步提交,保证配置即时生效(区别于apply异步) + editor.commit(); + LogUtils.d(TAG, "saveSFTPAuthModelToSP()执行完成,SFTP配置已成功存入SP"); + } catch (Exception e) { + LogUtils.e(TAG, "saveSFTPAuthModelToSP()执行异常:" + e.getMessage(), e); + } + } +} + diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/models/FTPAuthModel.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/models/SFTPAuthModel.java similarity index 57% rename from libappbase/src/main/java/cc/winboll/studio/libappbase/models/FTPAuthModel.java rename to libappbase/src/main/java/cc/winboll/studio/libappbase/models/SFTPAuthModel.java index 7ce07e8..38f7da8 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/models/FTPAuthModel.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/models/SFTPAuthModel.java @@ -1,45 +1,43 @@ package cc.winboll.studio.libappbase.models; /** - * FTP登录验证信息实体类 - * 封装FTP登录所需的所有配置信息:服务端地址、端口、账号密码、秘钥信息、传输模式、编码 + * SFTP登录验证信息实体类 + * 封装SFTP登录所需的所有配置信息:服务端地址、端口、账号密码、秘钥信息、编码 * @Author 豆包&ZhanGSKen - * @Date 2026/01/30 19:08 + * @Date 2026/01/30 19:08:00 + * @LastEditTime 2026/01/31 22:45:00 */ -public class FTPAuthModel { - public static final String TAG = "FTPAuthModel"; +public class SFTPAuthModel { + public static final String TAG = "SFTPAuthModel"; - // FTP服务器地址(必填,如192.168.1.100、ftp.xxx.com) + // SFTP服务器地址(必填,如192.168.1.100、sftp.xxx.com) private String ftpServer; - // FTP服务器端口(必填,默认21) - private int ftpPort = 21; - // FTP登录用户名(匿名登录传null/空) + // SFTP服务器端口(必填,默认22) + private int ftpPort = 22; + // SFTP登录用户名(匿名登录传null/空) private String ftpUsername; - // FTP登录密码(匿名登录传null/空) + // SFTP登录密码(匿名登录传null/空) private String ftpPassword; - // FTP登录秘钥路径(秘钥登录时使用,本地绝对路径,如/sdcard/ftp/key.pem,账号密码登录传null/空) + // SFTP登录秘钥路径(秘钥登录时使用,本地绝对路径,如/sdcard/sftp/key.pem,账号密码登录传null/空) private String ftpKeyPath; - // FTP登录秘钥密码(秘钥有密码时填写,无密码传null/空) + // SFTP登录秘钥密码(秘钥有密码时填写,无密码传null/空) private String ftpKeyPwd; - // 是否为主动模式(true=主动模式,false=被动模式<默认推荐>) - private boolean isActiveMode = false; - // FTP编码(默认UTF-8,解决中文文件名乱码) + // SFTP编码(默认UTF-8,解决中文文件名乱码) private String ftpCharset = "UTF-8"; // 空参构造(JavaBean规范) - public FTPAuthModel() { + public SFTPAuthModel() { } // 全参构造(快速初始化) - public FTPAuthModel(String ftpServer, int ftpPort, String ftpUsername, String ftpPassword, - String ftpKeyPath, String ftpKeyPwd, boolean isActiveMode, String ftpCharset) { + public SFTPAuthModel(String ftpServer, int ftpPort, String ftpUsername, String ftpPassword, + String ftpKeyPath, String ftpKeyPwd, String ftpCharset) { this.ftpServer = ftpServer; this.ftpPort = ftpPort; this.ftpUsername = ftpUsername; this.ftpPassword = ftpPassword; this.ftpKeyPath = ftpKeyPath; this.ftpKeyPwd = ftpKeyPwd; - this.isActiveMode = isActiveMode; this.ftpCharset = ftpCharset; } @@ -92,14 +90,6 @@ public class FTPAuthModel { this.ftpKeyPwd = ftpKeyPwd; } - public boolean isActiveMode() { - return isActiveMode; - } - - public void setActiveMode(boolean activeMode) { - isActiveMode = activeMode; - } - public String getFtpCharset() { return ftpCharset; } 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 9a35872..d169085 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 @@ -16,54 +16,63 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import cc.winboll.studio.libappbase.LogUtils; -import cc.winboll.studio.libappbase.models.FTPAuthModel; +import cc.winboll.studio.libappbase.models.SFTPAuthModel; /** * 文件备份工具类(单例模式) - * 区分应用Data目录/SDCard目录双Map管理备份文件路径 - * 核心功能:文件添加/移除 + ZIP打包 + FTP分步式上传(登录→传输→登出) - * 依赖:FTPUtils(单例)、FTPAuthModel(外部实体类)、Android上下文 - * 兼容:Java7、Android 6.0+,无第三方依赖(ZIP为原生实现) + * 区分应用Data目录/应用专属外部文件目录双Map管理备份文件路径 + * 核心功能:文件添加/移除 + ZIP打包(分data/sdcard目录) + SFTP分步式上传(登录→传输→登出) + * 依赖:FTPUtils(单例)、SFTPAuthModel(外部实体类)、Android上下文 + * 兼容:Java7、Android 6.0+,无第三方依赖(ZIP为原生实现),免动态读写权限 * @Author 豆包&ZhanGSKen - * @Date 2026/01/30 20:18 + * @Date 2026/01/30 20:18:00 + * @LastEditTime 2026/02/01 01:05:00 */ public class BackupUtils { public static final String TAG = "BackupUtils"; + // ZIP内部分级目录常量(统一维护,便于修改) + private static final String ZIP_DIR_DATA = "data/"; + private static final String ZIP_DIR_SDCARD = "sdcard/"; // 单例实例(双重校验锁,volatile保证可见性,线程安全) private static volatile BackupUtils sInstance; // 双Map分目录管理:key=文件唯一标识,value=对应目录下的相对路径 - private final Map mDataDirFileMap; // 基础根目录:应用Data目录 - private final Map mSdcardFileMap; // 基础根目录:/sdcard + private final Map mDataDirFileMap; // 基础根目录:应用私有Data目录(/data/data/[包名]/files) + private final Map mSdcardFileMap; // 基础根目录:应用专属外部文件目录(/storage/emulated/0/Android/data/[包名]/files) // 全局上下文(持有Application上下文,避免Activity内存泄漏) private Context mAppContext; - // FTP认证配置(直接引用外部实体类,无内部封装) - private FTPAuthModel mFtpAuthModel; - // FTP服务器指定上传目录(独立参数传入,标准化后作为成员变量) + // SFTP认证配置(直接引用外部实体类,无内部封装) + private SFTPAuthModel mFtpAuthModel; + // SFTP服务器指定上传目录(独立参数传入,标准化后作为成员变量) private String mFtpTargetDir; + // 应用专属外部文件目录(SDCard Map的基础根目录,初始化时赋值,避免重复创建) + private File mAppExternalFilesDir; - // 私有构造器:禁止外部实例化,初始化双Map+配置参数+标准化FTP上传目录 - private BackupUtils(Context context, FTPAuthModel ftpAuthModel, String ftpTargetDir) { + // 私有构造器:禁止外部实例化,初始化双Map+配置参数+标准化SFTP上传目录 + private BackupUtils(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir) { this.mAppContext = context.getApplicationContext(); this.mFtpAuthModel = ftpAuthModel; - // 标准化FTP上传目录:空则默认/,非空则补全结尾斜杠 + // 初始化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<>(); - LogUtils.d(TAG, "BackupUtils初始化完成 → FTP服务器:" + ftpAuthModel.getFtpServer() + ":" + ftpAuthModel.getFtpPort() + " | 上传目录:" + mFtpTargetDir); + LogUtils.d(TAG, "BackupUtils初始化完成 → SFTP服务器:" + ftpAuthModel.getFtpServer() + ":" + ftpAuthModel.getFtpPort() + " | 上传目录:" + mFtpTargetDir); + LogUtils.d(TAG, "SDCard Map基础根目录:" + (mAppExternalFilesDir == null ? "获取失败" : mAppExternalFilesDir.getAbsolutePath())); } /** * 单例初始化方法(必须先调用,否则getInstance()会抛异常) * @param context 上下文(推荐传Application,避免内存泄漏) - * @param ftpAuthModel 外部FTP认证实体类(含服务器/账号/端口/传输模式等) - * @param ftpTargetDir FTP服务器指定上传目录(如/backup,自动补全斜杠) + * @param ftpAuthModel 外部SFTP认证实体类(含服务器/账号/端口等) + * @param ftpTargetDir SFTP服务器指定上传目录(如/backup,自动补全斜杠) * @return BackupUtils单例实例 */ - public static BackupUtils getInstance(Context context, FTPAuthModel ftpAuthModel, String ftpTargetDir) { + public static BackupUtils getInstance(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir) { if (sInstance == null) { synchronized (BackupUtils.class) { if (sInstance == null) { @@ -72,7 +81,7 @@ public class BackupUtils { throw new IllegalArgumentException("初始化失败:Context 不能为空"); } if (ftpAuthModel == null || TextUtils.isEmpty(ftpAuthModel.getFtpServer())) { - throw new IllegalArgumentException("初始化失败:FTPAuthModel/ftpServer 不能为空"); + throw new IllegalArgumentException("初始化失败:SFTPAuthModel/ftpServer 不能为空"); } sInstance = new BackupUtils(context, ftpAuthModel, ftpTargetDir); } @@ -87,7 +96,7 @@ public class BackupUtils { */ public static BackupUtils getInstance() { if (sInstance == null) { - throw new IllegalStateException("BackupUtils未初始化,请先调用getInstance(Context, FTPAuthModel, String)"); + throw new IllegalStateException("BackupUtils未初始化,请先调用getInstance(Context, SFTPAuthModel, String)"); } return sInstance; } @@ -96,7 +105,7 @@ public class BackupUtils { /** * 添加应用Data目录下的备份文件(相对路径) * @param key 文件唯一标识(如log_20260130,避免重复) - * @param relativePath Data目录下的相对路径,如:files/log/app.log + * @param relativePath Data目录下的相对路径,如:log/app.log */ public void addDataDirFile(String key, String relativePath) { if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(relativePath)) { @@ -141,32 +150,32 @@ public class BackupUtils { LogUtils.d(TAG, "清空Data目录所有备份文件"); } - // ====================================== SDCard目录 - Map操作方法 ====================================== + // ====================================== 应用专属外部文件目录 - Map操作方法 ====================================== /** - * 添加SDCard目录下的备份文件(相对路径) + * 添加应用专属外部文件目录下的备份文件(相对路径) * @param key 文件唯一标识(如crash_20260130,避免重复) - * @param relativePath SDCard目录下的相对路径,如:winboll/backup/crash.log + * @param relativePath 外部文件目录下的相对路径,如:backup/crash.log */ public void addSdcardFile(String key, String relativePath) { - if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(relativePath) && isSdcardMounted()) { + if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(relativePath) && mAppExternalFilesDir != null) { mSdcardFileMap.put(key, relativePath); - LogUtils.d(TAG, "添加SDCard目录文件:" + key + " → " + relativePath); + LogUtils.d(TAG, "添加外部文件目录文件:" + key + " → " + relativePath); } } /** - * 移除SDCard目录下的指定备份文件 + * 移除应用专属外部文件目录下的指定备份文件 * @param key 文件唯一标识 */ public void removeSdcardFile(String key) { if (!TextUtils.isEmpty(key) && mSdcardFileMap.containsKey(key)) { mSdcardFileMap.remove(key); - LogUtils.d(TAG, "移除SDCard目录文件:" + key); + LogUtils.d(TAG, "移除外部文件目录文件:" + key); } } /** - * 获取SDCard目录下指定标识的文件相对路径 + * 获取应用专属外部文件目录下指定标识的文件相对路径 * @param key 文件唯一标识 * @return 相对路径,无则返回null */ @@ -175,7 +184,7 @@ public class BackupUtils { } /** - * 获取SDCard目录下所有备份文件(返回新Map,防止外部篡改原数据) + * 获取应用专属外部文件目录下所有备份文件(返回新Map,防止外部篡改原数据) * @return 只读Map副本 */ public Map getAllSdcardFiles() { @@ -183,29 +192,29 @@ public class BackupUtils { } /** - * 清空SDCard目录下的所有备份文件 + * 清空应用专属外部文件目录下的所有备份文件 */ public void clearSdcardFiles() { mSdcardFileMap.clear(); - LogUtils.d(TAG, "清空SDCard目录所有备份文件"); + LogUtils.d(TAG, "清空外部文件目录所有备份文件"); } - // ====================================== 核心方法:FTP分步式打包上传(登录→传输→登出) ====================================== + // ====================================== 核心方法:SFTP分步式打包上传(登录→传输→登出) ====================================== /** - * 核心方法:双Map文件打包为ZIP(UUID+时间戳)+ FTP分步上传 - * 执行流程:1.FTP登录 → 2.本地ZIP打包 → 3.FTP文件上传 → 4.FTP登出 + * 核心方法:双Map文件打包为ZIP(分data/sdcard目录,UUID+时间戳)+ SFTP分步上传 + * 执行流程:1.SFTP登录 → 2.本地ZIP打包 → 3.SFTP文件上传 → 4.SFTP登出 * 每步独立校验,异常即时返回,finally兜底释放所有资源 * @return true=全流程成功,false=任意步骤失败 * 注:必须在**子线程**执行(避免主线程阻塞ANR) */ public boolean packAndUploadByFtp() { - // 前置校验:无待备份文件/SDCard未挂载,直接返回失败 + // 前置校验:无待备份文件/外部文件目录获取失败,直接返回失败 if (mDataDirFileMap.isEmpty() && mSdcardFileMap.isEmpty()) { - LogUtils.e(TAG, "FTP上传失败:无待备份文件(DataDir+SDCard均为空)"); + LogUtils.e(TAG, "SFTP上传失败:无待备份文件(DataDir+外部文件目录均为空)"); return false; } - if (!isSdcardMounted()) { - LogUtils.e(TAG, "FTP上传失败:SDCard未挂载,无法创建临时ZIP文件"); + if (mAppExternalFilesDir == null) { + LogUtils.e(TAG, "SFTP上传失败:应用专属外部文件目录获取失败,无法访问文件"); return false; } @@ -214,7 +223,7 @@ public class BackupUtils { + "-" + System.currentTimeMillis() + ".zip"; // 本地临时ZIP文件:应用外部缓存目录(Android 6.0+免读写权限) File tempZipFile = new File(mAppContext.getExternalCacheDir(), zipFileName); - // FTP远程完整路径:标准化上传目录 + ZIP文件名(已提前补全斜杠,直接拼接) + // SFTP远程完整路径:标准化上传目录 + ZIP文件名(已提前补全斜杠,直接拼接) String remoteFtpFilePath = mFtpTargetDir + zipFileName; // 2. 获取FTPUtils单例(全局唯一) @@ -223,40 +232,40 @@ public class BackupUtils { boolean isUploadSuccess = false; try { - // ==================== 第一步:FTP服务器登录(基于外部FTPAuthModel)==================== - LogUtils.d(TAG, "开始FTP登录:" + mFtpAuthModel.getFtpServer() + ":" + mFtpAuthModel.getFtpPort()); + // ==================== 第一步:SFTP服务器登录(基于外部SFTPAuthModel)==================== + LogUtils.d(TAG, "开始SFTP登录:" + mFtpAuthModel.getFtpServer() + ":" + mFtpAuthModel.getFtpPort()); boolean isFtpLogin = ftpUtils.login(mFtpAuthModel); if (!isFtpLogin) { - LogUtils.e(TAG, "FTP上传失败:FTP登录失败(账号/密码/服务器/端口错误)"); + LogUtils.e(TAG, "SFTP上传失败:SFTP登录失败(账号/密码/服务器/端口错误)"); return false; } - LogUtils.i(TAG, "FTP登录成功,准备打包文件:" + zipFileName); + LogUtils.i(TAG, "SFTP登录成功,准备打包文件:" + zipFileName); - // ==================== 第二步:本地打包双Map文件为ZIP(Java7原生实现)==================== - LogUtils.d(TAG, "开始本地ZIP打包,临时文件路径:" + tempZipFile.getAbsolutePath()); + // ==================== 第二步:本地打包双Map文件为ZIP(分data/sdcard目录,Java7原生实现)==================== + LogUtils.d(TAG, "开始本地ZIP打包(分data/sdcard目录),临时文件路径:" + tempZipFile.getAbsolutePath()); boolean isPackSuccess = packFilesToZip(tempZipFile); if (!isPackSuccess || !tempZipFile.exists() || tempZipFile.length() == 0) { - LogUtils.e(TAG, "FTP上传失败:ZIP打包失败(文件不存在/空文件)"); + LogUtils.e(TAG, "SFTP上传失败:ZIP打包失败(文件不存在/空文件)"); return false; } LogUtils.i(TAG, "ZIP打包成功,文件大小:" + tempZipFile.length() / 1024 + "KB"); - // ==================== 第三步:FTP文件上传(调用现有FTPUtils.uploadFile)==================== - LogUtils.d(TAG, "开始FTP上传:本地→FTP" + remoteFtpFilePath); + // ==================== 第三步:SFTP文件上传(调用现有FTPUtils.uploadFile)==================== + LogUtils.d(TAG, "开始SFTP上传:本地→SFTP" + remoteFtpFilePath); isUploadSuccess = ftpUtils.uploadFile(tempZipFile.getAbsolutePath(), remoteFtpFilePath); if (isUploadSuccess) { - LogUtils.i(TAG, "FTP上传全流程成功:" + remoteFtpFilePath); + LogUtils.i(TAG, "SFTP上传全流程成功:" + remoteFtpFilePath); } else { - LogUtils.e(TAG, "FTP上传失败:文件传输到服务器失败(响应码异常/权限不足)"); + LogUtils.e(TAG, "SFTP上传失败:文件传输到服务器失败(响应码异常/权限不足)"); } } catch (Exception e) { // 捕获所有运行时异常,避免崩溃 - LogUtils.e(TAG, "FTP上传异常:" + e.getMessage(), e); + LogUtils.e(TAG, "SFTP上传异常:" + e.getMessage(), e); isUploadSuccess = false; } finally { // ==================== 最终兜底:无论成功/失败,释放所有资源 ==================== - // 1. FTP登出+断开连接(避免服务端连接数耗尽) + // 1. SFTP登出+断开连接(避免服务端连接数耗尽) if (ftpUtils.isConnected()) { ftpUtils.logout(); } @@ -273,10 +282,9 @@ public class BackupUtils { return isUploadSuccess; } - // ====================================== 私有工具方法:ZIP打包/SDCard校验 ====================================== + // ====================================== 私有工具方法:ZIP打包/目录校验 ====================================== /** - * Java7原生ZIP打包:遍历双Map,拼接绝对路径,写入临时ZIP文件 - * 自动跳过不存在的文件,ZIP内保留原相对路径(避免文件混乱) + * Java7原生ZIP打包:分目录打包(data/sdcard),遍历双Map文件写入ZIP * @param zipFile 生成的临时ZIP文件 * @return true=打包成功,false=打包失败 */ @@ -287,10 +295,16 @@ public class BackupUtils { zos = new ZipOutputStream(new FileOutputStream(zipFile), Charset.forName("UTF-8")); zos.setLevel(ZipOutputStream.DEFLATED); // 开启压缩,减小文件体积 - // 1. 打包应用Data目录下的文件 - packDirFilesToZip(zos, mDataDirFileMap, mAppContext.getFilesDir()); - // 2. 打包SDCard目录下的文件 - packDirFilesToZip(zos, mSdcardFileMap, Environment.getExternalStorageDirectory()); + // 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 + "子目录"); + } zos.flush(); return true; @@ -310,38 +324,41 @@ public class BackupUtils { } /** - * 批量打包指定目录下的文件到ZIP流 + * 批量打包指定目录下的文件到ZIP指定子目录 * @param zos ZIP输出流 * @param fileMap 待打包文件Map(key=标识,value=相对路径) - * @param baseDir 基础根目录(Data/SDCard) + * @param baseDir 本地基础根目录(Data/应用专属外部文件目录) + * @param zipSubDir ZIP内的目标子目录(如data/、sdcard/) */ - private void packDirFilesToZip(ZipOutputStream zos, Map fileMap, File baseDir) { + 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流 + // 拼接ZIP内的最终路径:子目录 + 原相对路径(如data/log/app.log) + String zipInnerPath = zipSubDir + relativePath; + // 将单个文件写入ZIP指定子目录 try { - addSingleFileToZip(zos, localFile, relativePath); + addSingleFileToZip(zos, localFile, zipInnerPath); } catch (IOException e) { - LogUtils.e(TAG, "打包单个文件失败:" + relativePath, e); + LogUtils.e(TAG, "打包单个文件失败:" + zipInnerPath, e); } } } /** - * 将单个文件写入ZIP流,保留相对路径作为ZIP内路径 + * 将单个文件写入ZIP指定路径 * @param zos ZIP输出流 * @param localFile 本地待打包文件 - * @param zipInnerPath ZIP内的相对路径 + * @param zipInnerPath ZIP内的最终路径(含子目录,如data/log/app.log) */ private void addSingleFileToZip(ZipOutputStream zos, File localFile, String zipInnerPath) throws IOException { ZipEntry zipEntry = new ZipEntry(zipInnerPath); @@ -357,13 +374,5 @@ public class BackupUtils { fis.close(); zos.closeEntry(); } - - /** - * 判断SDCard是否挂载且可读写 - * @return true=可用,false=不可用 - */ - private boolean isSdcardMounted() { - return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); - } } diff --git a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/FTPUtils.java b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/FTPUtils.java index f907da5..7e1ea28 100644 --- a/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/FTPUtils.java +++ b/libappbase/src/main/java/cc/winboll/studio/libappbase/utils/FTPUtils.java @@ -1,7 +1,7 @@ package cc.winboll.studio.libappbase.utils; import cc.winboll.studio.libappbase.LogUtils; -import cc.winboll.studio.libappbase.models.FTPAuthModel; +import cc.winboll.studio.libappbase.models.SFTPAuthModel; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; @@ -78,7 +78,7 @@ public class FTPUtils { * @param ftpAuthModel 登录配置实体类(不能为空,端口默认22,编码默认UTF-8) * @return 登录成功返回true,失败false */ - public boolean login(FTPAuthModel ftpAuthModel) { + public boolean login(SFTPAuthModel ftpAuthModel) { // 1. 实体类非空校验 if (ftpAuthModel == null) { LogUtils.e(TAG, "SFTP登录失败:FTPAuthModel实体类为null"); @@ -156,7 +156,7 @@ public class FTPUtils { */ @Deprecated public boolean login(String host, int port, String username, String password) { - FTPAuthModel ftpAuthModel = new FTPAuthModel(); + SFTPAuthModel ftpAuthModel = new SFTPAuthModel(); ftpAuthModel.setFtpServer(host); ftpAuthModel.setFtpPort(port <= 0 ? DEFAULT_SFTP_PORT : port); ftpAuthModel.setFtpUsername(username); diff --git a/libappbase/src/main/res/drawable/shape_edittext_bg.xml b/libappbase/src/main/res/drawable/shape_edittext_bg.xml new file mode 100644 index 0000000..d284d6a --- /dev/null +++ b/libappbase/src/main/res/drawable/shape_edittext_bg.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/libappbase/src/main/res/layout/activity_ftp_backups.xml b/libappbase/src/main/res/layout/activity_ftp_backups.xml index c2c1573..e1a167b 100644 --- a/libappbase/src/main/res/layout/activity_ftp_backups.xml +++ b/libappbase/src/main/res/layout/activity_ftp_backups.xml @@ -9,11 +9,18 @@ + android:layout_height="wrap_content" + android:gravity="right"> + +