应用备份打包上传功能完成
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#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
|
stageCount=12
|
||||||
libraryProject=libappbase
|
libraryProject=libappbase
|
||||||
baseVersion=15.15
|
baseVersion=15.15
|
||||||
publishVersion=15.15.11
|
publishVersion=15.15.11
|
||||||
buildCount=1
|
buildCount=17
|
||||||
baseBetaVersion=15.15.12
|
baseBetaVersion=15.15.12
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#Created by .winboll/winboll_app_build.gradle
|
#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
|
stageCount=12
|
||||||
libraryProject=libappbase
|
libraryProject=libappbase
|
||||||
baseVersion=15.15
|
baseVersion=15.15
|
||||||
publishVersion=15.15.11
|
publishVersion=15.15.11
|
||||||
buildCount=1
|
buildCount=17
|
||||||
baseBetaVersion=15.15.12
|
baseBetaVersion=15.15.12
|
||||||
|
|||||||
@@ -2,26 +2,151 @@ package cc.winboll.studio.libappbase.activities;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
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;
|
||||||
|
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<zhangsken@qq.com>
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
* @Date 2026/01/30 20:55
|
* @Date 2026/01/30 20:55
|
||||||
*/
|
*/
|
||||||
public class FTPBackupsActivity extends Activity {
|
public class FTPBackupsActivity extends Activity {
|
||||||
|
|
||||||
public static final String TAG = "FTPBackupsActivity";
|
public static final String TAG = "FTPBackupsActivity";
|
||||||
|
// 主线程Handler:子线程更新UI专用
|
||||||
|
private Handler mMainHandler;
|
||||||
|
// FTP服务器上传目标目录(可根据业务自定义)
|
||||||
|
private static final String FTP_TARGET_DIR = "/WinBoLLStudio/APPBackups/WinBoLL/";
|
||||||
|
|
||||||
@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崩溃
|
||||||
|
mMainHandler = new Handler(Looper.getMainLooper());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onSFTPSettings(View view) {
|
||||||
|
SFTPBackupsSettingsDialog dlg = new SFTPBackupsSettingsDialog(this);
|
||||||
|
dlg.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点击事件:执行FTP备份(核心调用方法)
|
||||||
|
* 主线程仅做UI触发,所有核心逻辑在子线程执行
|
||||||
|
*/
|
||||||
public void onBackups(View view) {
|
public void onBackups(View view) {
|
||||||
ToastUtils.show("onBackups");
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<zhangsken@qq.com>
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,45 +1,43 @@
|
|||||||
package cc.winboll.studio.libappbase.models;
|
package cc.winboll.studio.libappbase.models;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FTP登录验证信息实体类
|
* SFTP登录验证信息实体类
|
||||||
* 封装FTP登录所需的所有配置信息:服务端地址、端口、账号密码、秘钥信息、传输模式、编码
|
* 封装SFTP登录所需的所有配置信息:服务端地址、端口、账号密码、秘钥信息、编码
|
||||||
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
* @Author 豆包&ZhanGSKen<zhangsken@qq.com>
|
||||||
* @Date 2026/01/30 19:08
|
* @Date 2026/01/30 19:08:00
|
||||||
|
* @LastEditTime 2026/01/31 22:45:00
|
||||||
*/
|
*/
|
||||||
public class FTPAuthModel {
|
public class SFTPAuthModel {
|
||||||
public static final String TAG = "FTPAuthModel";
|
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;
|
private String ftpServer;
|
||||||
// FTP服务器端口(必填,默认21)
|
// SFTP服务器端口(必填,默认22)
|
||||||
private int ftpPort = 21;
|
private int ftpPort = 22;
|
||||||
// FTP登录用户名(匿名登录传null/空)
|
// SFTP登录用户名(匿名登录传null/空)
|
||||||
private String ftpUsername;
|
private String ftpUsername;
|
||||||
// FTP登录密码(匿名登录传null/空)
|
// SFTP登录密码(匿名登录传null/空)
|
||||||
private String ftpPassword;
|
private String ftpPassword;
|
||||||
// FTP登录秘钥路径(秘钥登录时使用,本地绝对路径,如/sdcard/ftp/key.pem,账号密码登录传null/空)
|
// SFTP登录秘钥路径(秘钥登录时使用,本地绝对路径,如/sdcard/sftp/key.pem,账号密码登录传null/空)
|
||||||
private String ftpKeyPath;
|
private String ftpKeyPath;
|
||||||
// FTP登录秘钥密码(秘钥有密码时填写,无密码传null/空)
|
// SFTP登录秘钥密码(秘钥有密码时填写,无密码传null/空)
|
||||||
private String ftpKeyPwd;
|
private String ftpKeyPwd;
|
||||||
// 是否为主动模式(true=主动模式,false=被动模式<默认推荐>)
|
// SFTP编码(默认UTF-8,解决中文文件名乱码)
|
||||||
private boolean isActiveMode = false;
|
|
||||||
// FTP编码(默认UTF-8,解决中文文件名乱码)
|
|
||||||
private String ftpCharset = "UTF-8";
|
private String ftpCharset = "UTF-8";
|
||||||
|
|
||||||
// 空参构造(JavaBean规范)
|
// 空参构造(JavaBean规范)
|
||||||
public FTPAuthModel() {
|
public SFTPAuthModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全参构造(快速初始化)
|
// 全参构造(快速初始化)
|
||||||
public FTPAuthModel(String ftpServer, int ftpPort, String ftpUsername, String ftpPassword,
|
public SFTPAuthModel(String ftpServer, int ftpPort, String ftpUsername, String ftpPassword,
|
||||||
String ftpKeyPath, String ftpKeyPwd, boolean isActiveMode, String ftpCharset) {
|
String ftpKeyPath, String ftpKeyPwd, String ftpCharset) {
|
||||||
this.ftpServer = ftpServer;
|
this.ftpServer = ftpServer;
|
||||||
this.ftpPort = ftpPort;
|
this.ftpPort = ftpPort;
|
||||||
this.ftpUsername = ftpUsername;
|
this.ftpUsername = ftpUsername;
|
||||||
this.ftpPassword = ftpPassword;
|
this.ftpPassword = ftpPassword;
|
||||||
this.ftpKeyPath = ftpKeyPath;
|
this.ftpKeyPath = ftpKeyPath;
|
||||||
this.ftpKeyPwd = ftpKeyPwd;
|
this.ftpKeyPwd = ftpKeyPwd;
|
||||||
this.isActiveMode = isActiveMode;
|
|
||||||
this.ftpCharset = ftpCharset;
|
this.ftpCharset = ftpCharset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,14 +90,6 @@ public class FTPAuthModel {
|
|||||||
this.ftpKeyPwd = ftpKeyPwd;
|
this.ftpKeyPwd = ftpKeyPwd;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isActiveMode() {
|
|
||||||
return isActiveMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setActiveMode(boolean activeMode) {
|
|
||||||
isActiveMode = activeMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFtpCharset() {
|
public String getFtpCharset() {
|
||||||
return ftpCharset;
|
return ftpCharset;
|
||||||
}
|
}
|
||||||
@@ -16,54 +16,63 @@ import java.util.zip.ZipEntry;
|
|||||||
import java.util.zip.ZipOutputStream;
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
import cc.winboll.studio.libappbase.LogUtils;
|
||||||
import cc.winboll.studio.libappbase.models.FTPAuthModel;
|
import cc.winboll.studio.libappbase.models.SFTPAuthModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件备份工具类(单例模式)
|
* 文件备份工具类(单例模式)
|
||||||
* 区分应用Data目录/SDCard目录双Map管理备份文件路径
|
* 区分应用Data目录/应用专属外部文件目录双Map管理备份文件路径
|
||||||
* 核心功能:文件添加/移除 + ZIP打包 + FTP分步式上传(登录→传输→登出)
|
* 核心功能:文件添加/移除 + ZIP打包(分data/sdcard目录) + SFTP分步式上传(登录→传输→登出)
|
||||||
* 依赖:FTPUtils(单例)、FTPAuthModel(外部实体类)、Android上下文
|
* 依赖:FTPUtils(单例)、SFTPAuthModel(外部实体类)、Android上下文
|
||||||
* 兼容: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
|
* @Date 2026/01/30 20:18:00
|
||||||
|
* @LastEditTime 2026/02/01 01:05:00
|
||||||
*/
|
*/
|
||||||
public class BackupUtils {
|
public class BackupUtils {
|
||||||
public static final String TAG = "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保证可见性,线程安全)
|
// 单例实例(双重校验锁,volatile保证可见性,线程安全)
|
||||||
private static volatile BackupUtils sInstance;
|
private static volatile BackupUtils sInstance;
|
||||||
|
|
||||||
// 双Map分目录管理:key=文件唯一标识,value=对应目录下的相对路径
|
// 双Map分目录管理:key=文件唯一标识,value=对应目录下的相对路径
|
||||||
private final Map<String, String> mDataDirFileMap; // 基础根目录:应用Data目录
|
private final Map<String, String> mDataDirFileMap; // 基础根目录:应用私有Data目录(/data/data/[包名]/files)
|
||||||
private final Map<String, String> mSdcardFileMap; // 基础根目录:/sdcard
|
private final Map<String, String> mSdcardFileMap; // 基础根目录:应用专属外部文件目录(/storage/emulated/0/Android/data/[包名]/files)
|
||||||
|
|
||||||
// 全局上下文(持有Application上下文,避免Activity内存泄漏)
|
// 全局上下文(持有Application上下文,避免Activity内存泄漏)
|
||||||
private Context mAppContext;
|
private Context mAppContext;
|
||||||
// FTP认证配置(直接引用外部实体类,无内部封装)
|
// SFTP认证配置(直接引用外部实体类,无内部封装)
|
||||||
private FTPAuthModel mFtpAuthModel;
|
private SFTPAuthModel mFtpAuthModel;
|
||||||
// FTP服务器指定上传目录(独立参数传入,标准化后作为成员变量)
|
// SFTP服务器指定上传目录(独立参数传入,标准化后作为成员变量)
|
||||||
private String mFtpTargetDir;
|
private String mFtpTargetDir;
|
||||||
|
// 应用专属外部文件目录(SDCard Map的基础根目录,初始化时赋值,避免重复创建)
|
||||||
|
private File mAppExternalFilesDir;
|
||||||
|
|
||||||
// 私有构造器:禁止外部实例化,初始化双Map+配置参数+标准化FTP上传目录
|
// 私有构造器:禁止外部实例化,初始化双Map+配置参数+标准化SFTP上传目录
|
||||||
private BackupUtils(Context context, FTPAuthModel ftpAuthModel, String ftpTargetDir) {
|
private BackupUtils(Context context, SFTPAuthModel ftpAuthModel, String ftpTargetDir) {
|
||||||
this.mAppContext = context.getApplicationContext();
|
this.mAppContext = context.getApplicationContext();
|
||||||
this.mFtpAuthModel = ftpAuthModel;
|
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 + "/");
|
this.mFtpTargetDir = TextUtils.isEmpty(ftpTargetDir) ? "/" : (ftpTargetDir.endsWith("/") ? ftpTargetDir : ftpTargetDir + "/");
|
||||||
// 初始化双Map(HashMap兼容Java7)
|
// 初始化双Map(HashMap兼容Java7)
|
||||||
mDataDirFileMap = new HashMap<>();
|
mDataDirFileMap = new HashMap<>();
|
||||||
mSdcardFileMap = 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()会抛异常)
|
* 单例初始化方法(必须先调用,否则getInstance()会抛异常)
|
||||||
* @param context 上下文(推荐传Application,避免内存泄漏)
|
* @param context 上下文(推荐传Application,避免内存泄漏)
|
||||||
* @param ftpAuthModel 外部FTP认证实体类(含服务器/账号/端口/传输模式等)
|
* @param ftpAuthModel 外部SFTP认证实体类(含服务器/账号/端口等)
|
||||||
* @param ftpTargetDir FTP服务器指定上传目录(如/backup,自动补全斜杠)
|
* @param ftpTargetDir SFTP服务器指定上传目录(如/backup,自动补全斜杠)
|
||||||
* @return BackupUtils单例实例
|
* @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) {
|
if (sInstance == null) {
|
||||||
synchronized (BackupUtils.class) {
|
synchronized (BackupUtils.class) {
|
||||||
if (sInstance == null) {
|
if (sInstance == null) {
|
||||||
@@ -72,7 +81,7 @@ public class BackupUtils {
|
|||||||
throw new IllegalArgumentException("初始化失败:Context 不能为空");
|
throw new IllegalArgumentException("初始化失败:Context 不能为空");
|
||||||
}
|
}
|
||||||
if (ftpAuthModel == null || TextUtils.isEmpty(ftpAuthModel.getFtpServer())) {
|
if (ftpAuthModel == null || TextUtils.isEmpty(ftpAuthModel.getFtpServer())) {
|
||||||
throw new IllegalArgumentException("初始化失败:FTPAuthModel/ftpServer 不能为空");
|
throw new IllegalArgumentException("初始化失败:SFTPAuthModel/ftpServer 不能为空");
|
||||||
}
|
}
|
||||||
sInstance = new BackupUtils(context, ftpAuthModel, ftpTargetDir);
|
sInstance = new BackupUtils(context, ftpAuthModel, ftpTargetDir);
|
||||||
}
|
}
|
||||||
@@ -87,7 +96,7 @@ public class BackupUtils {
|
|||||||
*/
|
*/
|
||||||
public static BackupUtils getInstance() {
|
public static BackupUtils getInstance() {
|
||||||
if (sInstance == null) {
|
if (sInstance == null) {
|
||||||
throw new IllegalStateException("BackupUtils未初始化,请先调用getInstance(Context, FTPAuthModel, String)");
|
throw new IllegalStateException("BackupUtils未初始化,请先调用getInstance(Context, SFTPAuthModel, String)");
|
||||||
}
|
}
|
||||||
return sInstance;
|
return sInstance;
|
||||||
}
|
}
|
||||||
@@ -96,7 +105,7 @@ public class BackupUtils {
|
|||||||
/**
|
/**
|
||||||
* 添加应用Data目录下的备份文件(相对路径)
|
* 添加应用Data目录下的备份文件(相对路径)
|
||||||
* @param key 文件唯一标识(如log_20260130,避免重复)
|
* @param key 文件唯一标识(如log_20260130,避免重复)
|
||||||
* @param relativePath Data目录下的相对路径,如:files/log/app.log
|
* @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)) {
|
||||||
@@ -141,32 +150,32 @@ public class BackupUtils {
|
|||||||
LogUtils.d(TAG, "清空Data目录所有备份文件");
|
LogUtils.d(TAG, "清空Data目录所有备份文件");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================================== SDCard目录 - Map操作方法 ======================================
|
// ====================================== 应用专属外部文件目录 - Map操作方法 ======================================
|
||||||
/**
|
/**
|
||||||
* 添加SDCard目录下的备份文件(相对路径)
|
* 添加应用专属外部文件目录下的备份文件(相对路径)
|
||||||
* @param key 文件唯一标识(如crash_20260130,避免重复)
|
* @param key 文件唯一标识(如crash_20260130,避免重复)
|
||||||
* @param relativePath SDCard目录下的相对路径,如:winboll/backup/crash.log
|
* @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) && isSdcardMounted()) {
|
if (!TextUtils.isEmpty(key) && !TextUtils.isEmpty(relativePath) && mAppExternalFilesDir != null) {
|
||||||
mSdcardFileMap.put(key, relativePath);
|
mSdcardFileMap.put(key, relativePath);
|
||||||
LogUtils.d(TAG, "添加SDCard目录文件:" + key + " → " + relativePath);
|
LogUtils.d(TAG, "添加外部文件目录文件:" + key + " → " + relativePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除SDCard目录下的指定备份文件
|
* 移除应用专属外部文件目录下的指定备份文件
|
||||||
* @param key 文件唯一标识
|
* @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);
|
||||||
LogUtils.d(TAG, "移除SDCard目录文件:" + key);
|
LogUtils.d(TAG, "移除外部文件目录文件:" + key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取SDCard目录下指定标识的文件相对路径
|
* 获取应用专属外部文件目录下指定标识的文件相对路径
|
||||||
* @param key 文件唯一标识
|
* @param key 文件唯一标识
|
||||||
* @return 相对路径,无则返回null
|
* @return 相对路径,无则返回null
|
||||||
*/
|
*/
|
||||||
@@ -175,7 +184,7 @@ public class BackupUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取SDCard目录下所有备份文件(返回新Map,防止外部篡改原数据)
|
* 获取应用专属外部文件目录下所有备份文件(返回新Map,防止外部篡改原数据)
|
||||||
* @return 只读Map副本
|
* @return 只读Map副本
|
||||||
*/
|
*/
|
||||||
public Map<String, String> getAllSdcardFiles() {
|
public Map<String, String> getAllSdcardFiles() {
|
||||||
@@ -183,29 +192,29 @@ public class BackupUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清空SDCard目录下的所有备份文件
|
* 清空应用专属外部文件目录下的所有备份文件
|
||||||
*/
|
*/
|
||||||
public void clearSdcardFiles() {
|
public void clearSdcardFiles() {
|
||||||
mSdcardFileMap.clear();
|
mSdcardFileMap.clear();
|
||||||
LogUtils.d(TAG, "清空SDCard目录所有备份文件");
|
LogUtils.d(TAG, "清空外部文件目录所有备份文件");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================================== 核心方法:FTP分步式打包上传(登录→传输→登出) ======================================
|
// ====================================== 核心方法:SFTP分步式打包上传(登录→传输→登出) ======================================
|
||||||
/**
|
/**
|
||||||
* 核心方法:双Map文件打包为ZIP(UUID+时间戳)+ FTP分步上传
|
* 核心方法:双Map文件打包为ZIP(分data/sdcard目录,UUID+时间戳)+ SFTP分步上传
|
||||||
* 执行流程:1.FTP登录 → 2.本地ZIP打包 → 3.FTP文件上传 → 4.FTP登出
|
* 执行流程:1.SFTP登录 → 2.本地ZIP打包 → 3.SFTP文件上传 → 4.SFTP登出
|
||||||
* 每步独立校验,异常即时返回,finally兜底释放所有资源
|
* 每步独立校验,异常即时返回,finally兜底释放所有资源
|
||||||
* @return true=全流程成功,false=任意步骤失败
|
* @return true=全流程成功,false=任意步骤失败
|
||||||
* 注:必须在**子线程**执行(避免主线程阻塞ANR)
|
* 注:必须在**子线程**执行(避免主线程阻塞ANR)
|
||||||
*/
|
*/
|
||||||
public boolean packAndUploadByFtp() {
|
public boolean packAndUploadByFtp() {
|
||||||
// 前置校验:无待备份文件/SDCard未挂载,直接返回失败
|
// 前置校验:无待备份文件/外部文件目录获取失败,直接返回失败
|
||||||
if (mDataDirFileMap.isEmpty() && mSdcardFileMap.isEmpty()) {
|
if (mDataDirFileMap.isEmpty() && mSdcardFileMap.isEmpty()) {
|
||||||
LogUtils.e(TAG, "FTP上传失败:无待备份文件(DataDir+SDCard均为空)");
|
LogUtils.e(TAG, "SFTP上传失败:无待备份文件(DataDir+外部文件目录均为空)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!isSdcardMounted()) {
|
if (mAppExternalFilesDir == null) {
|
||||||
LogUtils.e(TAG, "FTP上传失败:SDCard未挂载,无法创建临时ZIP文件");
|
LogUtils.e(TAG, "SFTP上传失败:应用专属外部文件目录获取失败,无法访问文件");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,7 +223,7 @@ public class BackupUtils {
|
|||||||
+ "-" + System.currentTimeMillis() + ".zip";
|
+ "-" + System.currentTimeMillis() + ".zip";
|
||||||
// 本地临时ZIP文件:应用外部缓存目录(Android 6.0+免读写权限)
|
// 本地临时ZIP文件:应用外部缓存目录(Android 6.0+免读写权限)
|
||||||
File tempZipFile = new File(mAppContext.getExternalCacheDir(), zipFileName);
|
File tempZipFile = new File(mAppContext.getExternalCacheDir(), zipFileName);
|
||||||
// FTP远程完整路径:标准化上传目录 + ZIP文件名(已提前补全斜杠,直接拼接)
|
// SFTP远程完整路径:标准化上传目录 + ZIP文件名(已提前补全斜杠,直接拼接)
|
||||||
String remoteFtpFilePath = mFtpTargetDir + zipFileName;
|
String remoteFtpFilePath = mFtpTargetDir + zipFileName;
|
||||||
|
|
||||||
// 2. 获取FTPUtils单例(全局唯一)
|
// 2. 获取FTPUtils单例(全局唯一)
|
||||||
@@ -223,40 +232,40 @@ public class BackupUtils {
|
|||||||
boolean isUploadSuccess = false;
|
boolean isUploadSuccess = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ==================== 第一步:FTP服务器登录(基于外部FTPAuthModel)====================
|
// ==================== 第一步:SFTP服务器登录(基于外部SFTPAuthModel)====================
|
||||||
LogUtils.d(TAG, "开始FTP登录:" + 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) {
|
||||||
LogUtils.e(TAG, "FTP上传失败:FTP登录失败(账号/密码/服务器/端口错误)");
|
LogUtils.e(TAG, "SFTP上传失败:SFTP登录失败(账号/密码/服务器/端口错误)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
LogUtils.i(TAG, "FTP登录成功,准备打包文件:" + zipFileName);
|
LogUtils.i(TAG, "SFTP登录成功,准备打包文件:" + zipFileName);
|
||||||
|
|
||||||
// ==================== 第二步:本地打包双Map文件为ZIP(Java7原生实现)====================
|
// ==================== 第二步:本地打包双Map文件为ZIP(分data/sdcard目录,Java7原生实现)====================
|
||||||
LogUtils.d(TAG, "开始本地ZIP打包,临时文件路径:" + 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) {
|
||||||
LogUtils.e(TAG, "FTP上传失败:ZIP打包失败(文件不存在/空文件)");
|
LogUtils.e(TAG, "SFTP上传失败:ZIP打包失败(文件不存在/空文件)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
LogUtils.i(TAG, "ZIP打包成功,文件大小:" + tempZipFile.length() / 1024 + "KB");
|
LogUtils.i(TAG, "ZIP打包成功,文件大小:" + tempZipFile.length() / 1024 + "KB");
|
||||||
|
|
||||||
// ==================== 第三步:FTP文件上传(调用现有FTPUtils.uploadFile)====================
|
// ==================== 第三步:SFTP文件上传(调用现有FTPUtils.uploadFile)====================
|
||||||
LogUtils.d(TAG, "开始FTP上传:本地→FTP" + remoteFtpFilePath);
|
LogUtils.d(TAG, "开始SFTP上传:本地→SFTP" + remoteFtpFilePath);
|
||||||
isUploadSuccess = ftpUtils.uploadFile(tempZipFile.getAbsolutePath(), remoteFtpFilePath);
|
isUploadSuccess = ftpUtils.uploadFile(tempZipFile.getAbsolutePath(), remoteFtpFilePath);
|
||||||
if (isUploadSuccess) {
|
if (isUploadSuccess) {
|
||||||
LogUtils.i(TAG, "FTP上传全流程成功:" + remoteFtpFilePath);
|
LogUtils.i(TAG, "SFTP上传全流程成功:" + remoteFtpFilePath);
|
||||||
} else {
|
} else {
|
||||||
LogUtils.e(TAG, "FTP上传失败:文件传输到服务器失败(响应码异常/权限不足)");
|
LogUtils.e(TAG, "SFTP上传失败:文件传输到服务器失败(响应码异常/权限不足)");
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 捕获所有运行时异常,避免崩溃
|
// 捕获所有运行时异常,避免崩溃
|
||||||
LogUtils.e(TAG, "FTP上传异常:" + e.getMessage(), e);
|
LogUtils.e(TAG, "SFTP上传异常:" + e.getMessage(), e);
|
||||||
isUploadSuccess = false;
|
isUploadSuccess = false;
|
||||||
} finally {
|
} finally {
|
||||||
// ==================== 最终兜底:无论成功/失败,释放所有资源 ====================
|
// ==================== 最终兜底:无论成功/失败,释放所有资源 ====================
|
||||||
// 1. FTP登出+断开连接(避免服务端连接数耗尽)
|
// 1. SFTP登出+断开连接(避免服务端连接数耗尽)
|
||||||
if (ftpUtils.isConnected()) {
|
if (ftpUtils.isConnected()) {
|
||||||
ftpUtils.logout();
|
ftpUtils.logout();
|
||||||
}
|
}
|
||||||
@@ -273,10 +282,9 @@ public class BackupUtils {
|
|||||||
return isUploadSuccess;
|
return isUploadSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ====================================== 私有工具方法:ZIP打包/SDCard校验 ======================================
|
// ====================================== 私有工具方法:ZIP打包/目录校验 ======================================
|
||||||
/**
|
/**
|
||||||
* Java7原生ZIP打包:遍历双Map,拼接绝对路径,写入临时ZIP文件
|
* Java7原生ZIP打包:分目录打包(data/sdcard),遍历双Map文件写入ZIP
|
||||||
* 自动跳过不存在的文件,ZIP内保留原相对路径(避免文件混乱)
|
|
||||||
* @param zipFile 生成的临时ZIP文件
|
* @param zipFile 生成的临时ZIP文件
|
||||||
* @return true=打包成功,false=打包失败
|
* @return true=打包成功,false=打包失败
|
||||||
*/
|
*/
|
||||||
@@ -287,10 +295,16 @@ public class BackupUtils {
|
|||||||
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目录下的文件
|
// 1. 打包应用Data目录下的文件 → ZIP内的data子目录
|
||||||
packDirFilesToZip(zos, mDataDirFileMap, mAppContext.getFilesDir());
|
if (!mDataDirFileMap.isEmpty()) {
|
||||||
// 2. 打包SDCard目录下的文件
|
packDirFilesToZip(zos, mDataDirFileMap, mAppContext.getFilesDir(), ZIP_DIR_DATA);
|
||||||
packDirFilesToZip(zos, mSdcardFileMap, Environment.getExternalStorageDirectory());
|
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();
|
zos.flush();
|
||||||
return true;
|
return true;
|
||||||
@@ -310,38 +324,41 @@ public class BackupUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量打包指定目录下的文件到ZIP流
|
* 批量打包指定目录下的文件到ZIP指定子目录
|
||||||
* @param zos ZIP输出流
|
* @param zos ZIP输出流
|
||||||
* @param fileMap 待打包文件Map(key=标识,value=相对路径)
|
* @param fileMap 待打包文件Map(key=标识,value=相对路径)
|
||||||
* @param baseDir 基础根目录(Data/SDCard)
|
* @param baseDir 本地基础根目录(Data/应用专属外部文件目录)
|
||||||
|
* @param zipSubDir ZIP内的目标子目录(如data/、sdcard/)
|
||||||
*/
|
*/
|
||||||
private void packDirFilesToZip(ZipOutputStream zos, Map<String, String> fileMap, File baseDir) {
|
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流
|
// 拼接ZIP内的最终路径:子目录 + 原相对路径(如data/log/app.log)
|
||||||
|
String zipInnerPath = zipSubDir + relativePath;
|
||||||
|
// 将单个文件写入ZIP指定子目录
|
||||||
try {
|
try {
|
||||||
addSingleFileToZip(zos, localFile, relativePath);
|
addSingleFileToZip(zos, localFile, zipInnerPath);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LogUtils.e(TAG, "打包单个文件失败:" + relativePath, e);
|
LogUtils.e(TAG, "打包单个文件失败:" + zipInnerPath, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将单个文件写入ZIP流,保留相对路径作为ZIP内路径
|
* 将单个文件写入ZIP指定路径
|
||||||
* @param zos ZIP输出流
|
* @param zos ZIP输出流
|
||||||
* @param localFile 本地待打包文件
|
* @param localFile 本地待打包文件
|
||||||
* @param zipInnerPath ZIP内的相对路径
|
* @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);
|
||||||
@@ -357,13 +374,5 @@ public class BackupUtils {
|
|||||||
fis.close();
|
fis.close();
|
||||||
zos.closeEntry();
|
zos.closeEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断SDCard是否挂载且可读写
|
|
||||||
* @return true=可用,false=不可用
|
|
||||||
*/
|
|
||||||
private boolean isSdcardMounted() {
|
|
||||||
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package cc.winboll.studio.libappbase.utils;
|
package cc.winboll.studio.libappbase.utils;
|
||||||
|
|
||||||
import cc.winboll.studio.libappbase.LogUtils;
|
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.ChannelSftp;
|
||||||
import com.jcraft.jsch.JSch;
|
import com.jcraft.jsch.JSch;
|
||||||
import com.jcraft.jsch.JSchException;
|
import com.jcraft.jsch.JSchException;
|
||||||
@@ -78,7 +78,7 @@ public class FTPUtils {
|
|||||||
* @param ftpAuthModel 登录配置实体类(不能为空,端口默认22,编码默认UTF-8)
|
* @param ftpAuthModel 登录配置实体类(不能为空,端口默认22,编码默认UTF-8)
|
||||||
* @return 登录成功返回true,失败false
|
* @return 登录成功返回true,失败false
|
||||||
*/
|
*/
|
||||||
public boolean login(FTPAuthModel ftpAuthModel) {
|
public boolean login(SFTPAuthModel ftpAuthModel) {
|
||||||
// 1. 实体类非空校验
|
// 1. 实体类非空校验
|
||||||
if (ftpAuthModel == null) {
|
if (ftpAuthModel == null) {
|
||||||
LogUtils.e(TAG, "SFTP登录失败:FTPAuthModel实体类为null");
|
LogUtils.e(TAG, "SFTP登录失败:FTPAuthModel实体类为null");
|
||||||
@@ -156,7 +156,7 @@ public class FTPUtils {
|
|||||||
*/
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
public boolean login(String host, int port, String username, String password) {
|
public boolean login(String host, int port, String username, String password) {
|
||||||
FTPAuthModel ftpAuthModel = new FTPAuthModel();
|
SFTPAuthModel ftpAuthModel = new SFTPAuthModel();
|
||||||
ftpAuthModel.setFtpServer(host);
|
ftpAuthModel.setFtpServer(host);
|
||||||
ftpAuthModel.setFtpPort(port <= 0 ? DEFAULT_SFTP_PORT : port);
|
ftpAuthModel.setFtpPort(port <= 0 ? DEFAULT_SFTP_PORT : port);
|
||||||
ftpAuthModel.setFtpUsername(username);
|
ftpAuthModel.setFtpUsername(username);
|
||||||
|
|||||||
18
libappbase/src/main/res/drawable/shape_edittext_bg.xml
Normal file
18
libappbase/src/main/res/drawable/shape_edittext_bg.xml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_focused="true">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#FFFFFF"/>
|
||||||
|
<stroke android:width="1dp" android:color="#007AFF"/>
|
||||||
|
<corners android:radius="8dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#FFFFFF"/>
|
||||||
|
<stroke android:width="1dp" android:color="#E5E5E5"/>
|
||||||
|
<corners android:radius="8dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
|
|
||||||
@@ -9,11 +9,18 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="right">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:text="⚙️"
|
||||||
|
android:onClick="onSFTPSettings"/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="48dp"
|
||||||
android:text="Backups"
|
android:text="Backups"
|
||||||
android:onClick="onBackups"/>
|
android:onClick="onBackups"/>
|
||||||
|
|
||||||
|
|||||||
163
libappbase/src/main/res/layout/dialog_sftp_backup_settings.xml
Normal file
163
libappbase/src/main/res/layout/dialog_sftp_backup_settings.xml
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="20dp"
|
||||||
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="SFTP备份配置"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:layout_marginBottom="20dp"/>
|
||||||
|
|
||||||
|
<!-- 基础配置区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="15dp">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="基础配置"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="#666666"
|
||||||
|
android:layout_marginBottom="10dp"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_sftp_server"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:hint="SFTP服务器地址(IP/域名)"
|
||||||
|
android:paddingHorizontal="15dp"
|
||||||
|
android:background="@drawable/shape_edittext_bg"
|
||||||
|
android:layout_marginBottom="8dp"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_sftp_port"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:hint="端口号(默认22)"
|
||||||
|
android:inputType="number"
|
||||||
|
android:paddingHorizontal="15dp"
|
||||||
|
android:background="@drawable/shape_edittext_bg"
|
||||||
|
android:layout_marginBottom="8dp"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_sftp_username"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:hint="登录用户名"
|
||||||
|
android:paddingHorizontal="15dp"
|
||||||
|
android:background="@drawable/shape_edittext_bg"
|
||||||
|
android:layout_marginBottom="8dp"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_sftp_pwd"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:hint="登录密码"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:paddingHorizontal="15dp"
|
||||||
|
android:background="@drawable/shape_edittext_bg"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 秘钥配置区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="15dp">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="秘钥配置(可选,优先账号密码)"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="#666666"
|
||||||
|
android:layout_marginBottom="10dp"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_sftp_key_path"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:hint="秘钥本地路径(如/sdcard/key.pem)"
|
||||||
|
android:paddingHorizontal="15dp"
|
||||||
|
android:background="@drawable/shape_edittext_bg"
|
||||||
|
android:layout_marginBottom="8dp"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_sftp_key_pwd"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:hint="秘钥密码(无则留空)"
|
||||||
|
android:inputType="textPassword"
|
||||||
|
android:paddingHorizontal="15dp"
|
||||||
|
android:background="@drawable/shape_edittext_bg"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 高级配置区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="20dp">
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="高级配置"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="#666666"
|
||||||
|
android:layout_marginBottom="10dp"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_sftp_charset"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:hint="编码(默认UTF-8)"
|
||||||
|
android:paddingHorizontal="15dp"
|
||||||
|
android:background="@drawable/shape_edittext_bg"
|
||||||
|
android:text="UTF-8"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:spacing="10dp">
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_sftp_clear"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="清空"
|
||||||
|
android:backgroundTint="#FF9500"
|
||||||
|
android:textColor="#FFFFFF"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_sftp_cancel"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="取消"
|
||||||
|
android:backgroundTint="#999999"
|
||||||
|
android:textColor="#FFFFFF"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_sftp_save"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="保存"
|
||||||
|
android:backgroundTint="#007AFF"
|
||||||
|
android:textColor="#FFFFFF"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
Reference in New Issue
Block a user