杜洪波
3 天以前 1606264b2b90b0782f97df79d8cad2306529f407
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
package com.product.system.backup.service;
 
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
 
import org.springframework.stereotype.Service;
 
import com.alibaba.druid.util.StringUtils;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.SftpProgressMonitor;
import com.product.core.config.Global;
import com.product.system.backup.entity.BackupLogger;
 
/**
 *     系统备份
 *    数据库备份:每天执行代码备份
 *    附件备份:每天执行代码备份当天附件
 *    遗留问题:附件被修改,不能锁定非当天被修改的附件
 */
@Service("systemBackService")
public class SystemBackupService {
    
    // 备份配置
    Properties config;
 
    // 备份日志
    BackupLogger log;
    
    // 数字时间和数字日期(用做文件夹或者文件名)
    String NUMBER_TIME;    //例如(20250428091001)
    String NUMBER_DATE; //例如(20240428)
    
    // 数字日期格式
    SimpleDateFormat numberTimeFormat = new SimpleDateFormat("yyyyMMddHHmmss");
    SimpleDateFormat numberDateFormat = new SimpleDateFormat("yyyyMMdd");
 
    // 备份配置文件
    private static final String CONFIG_FILE_PATH = "systemBackup.properties";
    
    /**
     *     系统备份入口
     *     调用地点:定时任务配置功能(bean)
     */
    public void systemBackupInit(){
        // 初始日志文件
        log = new BackupLogger();
        // 在日志中记录操作系统信息,便于调试
        log.writeInfo("【系统备份入口】操作系统: " + System.getProperty("os.name"), BackupLogger.INFO_TYPE);
        log.writeInfo("【系统备份入口】文件分隔符: " + File.separator, BackupLogger.INFO_TYPE);
        log.writeInfo("【系统备份入口】系统备份开始..................", BackupLogger.INFO_TYPE);
        NUMBER_TIME = numberTimeFormat.format(new Date());
        NUMBER_DATE = numberDateFormat.format(new Date());
        log.writeInfo("【系统备份入口】系统日期:" + NUMBER_DATE, BackupLogger.INFO_TYPE);
        log.writeInfo("【系统备份入口】系统时间:" + NUMBER_TIME, BackupLogger.INFO_TYPE);
        // 进入备份进程
        backupProcess();
        log.closeLogger();
    }
 
    /**
     * 数据备份进程
     */
    public void backupProcess() {
        log.writeInfo("【系统备份进程】系统备份进程开始..................", BackupLogger.INFO_TYPE);
        long start = System.currentTimeMillis();
        boolean status= true;
        // 第一步:初始配文件参数,当前目生成时间戳的文件夹
        status = initSystemConfig();
        if (!status)
            return;
        // 第二步:执行数据库备份
        status = runExpDataBase();
        if (!status)
            return;
        // 第三步:执行对数据库文件、工程代码、上传文件做压缩备份
        status = zipDataBackup();
        if (!status)
            return;
        // 第四步:上传压缩备份文件到FTP
        status = sftpTransferService(true);
//        uploadBackupMachine2();
        if (!status)
            return;
        // 第五步:清除数据
//        clearKeepData();
 
        long end = System.currentTimeMillis();
        log.writeInfo("【系统备份进程】系统备份进程结束,耗时.................." + ((end - start) / 1000) + "秒", BackupLogger.INFO_TYPE);
        System.out.println("系统备份耗时.................." + ((end - start) / 1000) + "秒");
    }
 
    /**
     * 第一步:初始配置文件参数,当前目录生成时间戳的文件夹
     */
    public boolean initSystemConfig() {
        log.writeInfo("【初始配置文件】开始初始系统配置文件..................", BackupLogger.INFO_TYPE);
        try (InputStream  reader = getClass().getClassLoader().getResourceAsStream(CONFIG_FILE_PATH)) {
            if (reader == null) {
                log.writeInfo("【初始配置文件】初始系统配置文件失败:" + CONFIG_FILE_PATH + "配置文件不存在", BackupLogger.ERROR_TYPE);
            }
            // 读取备份配置文件
            config = new Properties();
            config.load(reader);
            // 获取系统文件路径
            String systemFileFolder = Global.getSystemConfig("local.dir", "");
            File newFile= new File(systemFileFolder);
            if (!newFile.isAbsolute()) {
                systemFileFolder = System.getProperty("user.dir") + File.separator + systemFileFolder;
            }
            // 系统附件存放目录(当天附件)
            config.setProperty("DOCUMENT_ROOT", systemFileFolder + File.separator + "00000000-0000-0000-0000-000000000000" + File.separator + NUMBER_DATE);
            // 数据库备份目录(数据库备份根目录+时间文件名+.sql)
            config.setProperty("DATABASE_BACKUP", config.getProperty("DATABASE_ROOT") + File.separator + NUMBER_TIME + ".sql");
            // 备份目标文件(ZIP备份根目录+时间文件名+.zip)
            config.setProperty("ZIPFILE_BACKUP", config.getProperty("ZIPFILE_ROOT") + File.separator + NUMBER_TIME + ".zip");
            // 创建ZIP文件根目录
            File zipFileRoot = new File(config.getProperty("ZIPFILE_ROOT"));
            if (!zipFileRoot.exists()) {
                if (!zipFileRoot.mkdirs()) {
                    log.writeInfo("【初始配置文件】无法创建目录: " + zipFileRoot.getAbsolutePath(), BackupLogger.ERROR_TYPE);
                    return false;
                }
            }
            log.writeInfo("【初始配置文件】DATABASE_ROOT=" + config.getProperty("DATABASE_ROOT"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】DATABASE_BACKUP=" + config.getProperty("DATABASE_BACKUP"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】DOCUMENT_ROOT=" + config.getProperty("DOCUMENT_ROOT"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】DOCUMENT_BACKUP=" + config.getProperty("DOCUMENT_BACKUP"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】ZIPFILE_ROOT=" + config.getProperty("ZIPFILE_ROOT"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】ZIPFILE_BACKUP=" + config.getProperty("ZIPFILE_BACKUP"), BackupLogger.INFO_TYPE);
            log.writeInfo("【初始配置文件】初始系统配置文件结束.................", BackupLogger.INFO_TYPE);
        } catch (Exception e) {
            e.printStackTrace();
            log.writeInfo("【初始配置文件】系统初始配置文件失败," + new File("config.properties").getAbsolutePath() + "," + e.getMessage(),
                    BackupLogger.ERROR_TYPE);
            return false;
        }
        return true;
    }
    
    /**
     *     第二步:备份数据库文件
     */
    public boolean runExpDataBase() {
        log.writeInfo("【备份数据库】开始备份数据库..................", BackupLogger.INFO_TYPE);
        String databaseHost = config.getProperty("DATABASE_HOST");
        String databasePort = config.getProperty("DATABASE_PORT");
        String databaseName = config.getProperty("DATABASE_NAME");
        String databaseUser = config.getProperty("DATABASE_USER");
        String databasePwd = config.getProperty("DATABASE_PWD");
        
        log.writeInfo("【备份数据库】数据库HOST:" + databaseHost, BackupLogger.INFO_TYPE);
        log.writeInfo("【备份数据库】数据库PORT:" + databasePort, BackupLogger.INFO_TYPE);
        
        File backupFile = new File(config.getProperty("DATABASE_BACKUP"));
        
        // 确保备份文件的目录存在
        if (!backupFile.getParentFile().exists()) {
            backupFile.getParentFile().mkdirs();
        }
        log.writeInfo("【备份数据库】数据库备份文件完整目录:" + backupFile.getAbsolutePath(), BackupLogger.INFO_TYPE);
 
        ProcessBuilder processBuilder = new ProcessBuilder(
            "mysqldump", 
            "-h" + databaseHost, 
            "-P" + databasePort, 
            "-u" + databaseUser, 
            "-p" + databasePwd, 
            databaseName
        );
 
        try {
            Process process = processBuilder.start();
            // 读取mysqldump的输出并写入到备份文件
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
                 FileWriter writer = new FileWriter(backupFile)) {
                
                String line;
                while ((line = reader.readLine()) != null) {
                    writer.write(line + System.lineSeparator());
                }
            }
 
            // 读取错误流并记录日志
            try (BufferedReader errorReader = new BufferedReader(
                new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
                String errorLine;
                while ((errorLine = errorReader.readLine()) != null) {
                    log.writeInfo("【备份数据库】错误输出: " + errorLine, BackupLogger.ERROR_TYPE);
                }
            }
     
            int exitCode = process.waitFor();
            if (exitCode == 0) {
                log.writeInfo("【备份数据库】数据库备份成功:" + backupFile.getAbsolutePath(), BackupLogger.INFO_TYPE);
//              if (!"127.0.0.1".equals(databaseHost)) {
                    // 获取数据库备份文件
//                    return sftpTransferService(false);
//                }
            } else {
                log.writeInfo("【备份数据库】数据库备份失败,退出码: " + exitCode, BackupLogger.ERROR_TYPE);
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.writeInfo("【备份数据库】数据库备份失败:" + e.getMessage(), BackupLogger.ERROR_TYPE);
            return false;
        }
        return true;
    }
 
    private static void addFolderToZip(File folder, String parentPath, ZipOutputStream zos) throws IOException {
        File[] files = folder.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    // 递归处理子目录,保持路径结构
                    addFolderToZip(file, parentPath + file.getName() + "/", zos);
                } else {
                    // 添加文件到ZIP,保持路径结构
                    addFileToZip(file, parentPath + file.getName(), zos);
                }
            }
        }
    }
    
    private static void addFileToZip(File file, String entryName, ZipOutputStream zos) throws IOException {
        try (FileInputStream fis = new FileInputStream(file)) {
            zos.putNextEntry(new ZipEntry(entryName));
            byte[] buffer = new byte[1024];
            int length;
            while ((length = fis.read(buffer)) >= 0) {
                zos.write(buffer, 0, length);
            }
            zos.closeEntry();
        }
    }
    
    /**
     *     第三步:压缩备份文件
     */
    public boolean zipDataBackup() {
        log.writeInfo("【压缩备份文件】开始压缩备份文件..................", BackupLogger.INFO_TYPE);
        String documentPath = config.getProperty("DOCUMENT_ROOT");
        String databasePath = config.getProperty("DATABASE_BACKUP");
        String zipFilePath = config.getProperty("ZIPFILE_BACKUP");
        log.writeInfo("【压缩备份文件】附件存储目录路径:" + documentPath, BackupLogger.INFO_TYPE);
        log.writeInfo("【压缩备份文件】数据库备份完整路径:" + databasePath, BackupLogger.INFO_TYPE);
        log.writeInfo("【压缩备份文件】备份压缩文件完整路径:" + zipFilePath, BackupLogger.INFO_TYPE);
        try (FileOutputStream fos = new FileOutputStream(zipFilePath);
               ZipOutputStream zos = new ZipOutputStream(fos)) {
            
            log.writeInfo("【压缩备份文件】压缩系统附件", BackupLogger.INFO_TYPE);
            // 压缩文件夹(保留完整路径结构)
            // 注意:这里我们传递了根目录名称作为初始parentPath
            addFolderToZip(new File(documentPath), NUMBER_DATE + "/", zos);
    
            log.writeInfo("【压缩备份文件】压缩数据库备份文件", BackupLogger.INFO_TYPE);
            // 压缩SQL文件(放在指定路径下)
            addFileToZip(new File(databasePath), "database/" + NUMBER_TIME + ".sql", zos);
    
            log.writeInfo("【压缩备份文件】压缩完成", BackupLogger.INFO_TYPE);
        } catch (IOException e) {
            e.printStackTrace();
            log.writeInfo("【压缩备份文件】压缩失败:" + e.getMessage(), BackupLogger.ERROR_TYPE);
            return false;
        }
        return true;
    }
    
    /**
     * SFTP连接:通过SFTP将备份文件上传备用机  或  从mysql服务器上获取备份文件
     * @param isUpload     是否上传
     * @return
     */
    public boolean sftpTransferService(boolean isUpload) {
        String sftpTitle = null;
        String localFilePath = null;
        String sftpFilePath = null;
        if(isUpload) {
            sftpTitle = "上传系统备份文件";
            localFilePath = config.getProperty("ZIPFILE_BACKUP"); // 本地文件路径
            sftpFilePath = config.getProperty("UPLOAD_BACKUP_DIR") + NUMBER_TIME + ".zip"; // SFTP服务文件路径
        } else {
//            sftpTitle = "获取数据库备份文件";
//            localFilePath = config.getProperty("DATABASE_BACKUP"); // 本地文件路径
//            sftpFilePath = config.getProperty("DATABASE_BACKUP"); // SFTP服务文件路径
        }
        String host = config.getProperty("UPLOAD_SFTP_HOST"); // SFTP服务IP地址
        String port = config.getProperty("UPLOAD_SFTP_PORT"); // SSH端口
        String user = config.getProperty("UPLOAD_SFTP_USER"); // SFTP服务的用户名
        String password = config.getProperty("UPLOAD_SFTP_PWD"); // SFTP服务的密码
        log.writeInfo(String.format("【%s】开始%s..................", sftpTitle, sftpTitle), BackupLogger.INFO_TYPE);
        log.writeInfo(String.format("【%s】SFTP服务地址:", sftpTitle) + host, BackupLogger.INFO_TYPE);
        log.writeInfo(String.format("【%s】SFTP服务端口:", sftpTitle) + port, BackupLogger.INFO_TYPE);
        log.writeInfo(String.format("【%s】SFTP服务存储目录:", sftpTitle) + sftpFilePath, BackupLogger.INFO_TYPE);
        log.writeInfo(String.format("【%s】当前服务存储目录:", sftpTitle) + localFilePath, BackupLogger.INFO_TYPE);
        if (StringUtils.isEmpty(host) || StringUtils.isEmpty(port) || StringUtils.isEmpty(user) || StringUtils.isEmpty(password)) {
            log.writeInfo(String.format("【%s】SFTP信息未配置完整,不予连接传输文件", sftpTitle), BackupLogger.INFO_TYPE);
            return true;
        }
        try {
            JSch jsch = new JSch();
            Session session = jsch.getSession(user, host, Integer.valueOf(port));
            session.setPassword(password);
            session.setConfig("StrictHostKeyChecking", "no"); // 仅限测试环境
            session.connect(5000); // 设置连接超时时间
  
            Channel channel = session.openChannel("sftp");
            channel.connect(5000); // 设置通道超时时间
            ChannelSftp sftpChannel = (ChannelSftp) channel;
             
            // 确保目录存在(关键步骤)
            String remoteDir = sftpFilePath.substring(0, sftpFilePath.lastIndexOf('/'));
            if (!remoteDir.startsWith("/")) {
                remoteDir = "/" + remoteDir;
            }
            if(isUpload) {
                // 创建对应目录文件夹
                createRemoteDirectory(sftpChannel, remoteDir);
                sftpChannel.put(localFilePath, sftpFilePath, new SftpProgressMonitor() {
                    public void init(int op, String src, String dest, long max) {
                        log.writeInfo("【上传系统备份文件】开始传输: " + src + " -> " + dest, BackupLogger.INFO_TYPE);
                    }
                    public boolean count(long count) { return true; }
                    public void end() { log.writeInfo("【上传系统备份文件】传输完成", BackupLogger.INFO_TYPE); }
                });
            } else {
                // 创建对应目录文件夹
                File fileDirectory = new File(remoteDir);
                if (!fileDirectory.exists()) {
                    fileDirectory.mkdirs();
                }
                sftpChannel.get(localFilePath, sftpFilePath, new SftpProgressMonitor() {
                    public void init(int op, String src, String dest, long max) {
                        log.writeInfo("【获取数据库备份文件】开始传输: " + src + " -> " + dest, BackupLogger.INFO_TYPE);
                    }
                    public boolean count(long count) { return true; }
                    public void end() { log.writeInfo("【获取数据库备份文件】传输完成", BackupLogger.INFO_TYPE); }
                });
            }
            sftpChannel.exit();
            session.disconnect();
            log.writeInfo(String.format("【%s】%s成功", sftpTitle, sftpTitle), BackupLogger.INFO_TYPE);
        } catch (JSchException | SftpException e) {
            e.printStackTrace();
            log.writeInfo(String.format("【%s】%s失败:", sftpTitle, sftpTitle) + e.getMessage(), BackupLogger.ERROR_TYPE);
            return false;
        }
        return true;
    }
    
    /**
     *  递归创建远程目录
     * @param sftpChannel
     * @param remoteDir
     * @throws SftpException
     */
    private void createRemoteDirectory(ChannelSftp sftpChannel, String remoteDir) throws SftpException {
        try {
            sftpChannel.cd(remoteDir);
        } catch (SftpException e) {
            if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
                if (remoteDir.length() == 3 && remoteDir.charAt(1) == ':') {
                    // 跳过根目录
                    return;
                }
                int pos = remoteDir.lastIndexOf('/');
                if (pos > 0) {
                    createRemoteDirectory(sftpChannel, remoteDir.substring(0, pos));
                }
                sftpChannel.mkdir(remoteDir);
                log.writeInfo("【上传备份文件】创建目录成功: " + remoteDir, BackupLogger.INFO_TYPE);
            } else {
                throw e;
            }
        }
    }
}