|
@@ -0,0 +1,171 @@
|
|
|
+package org.dromara.system.service.dbbackup;
|
|
|
+
|
|
|
+import jakarta.servlet.http.HttpServletResponse;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.ibatis.jdbc.ScriptRunner;
|
|
|
+import org.dromara.system.util.FilteredCommentInputStream;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
+
|
|
|
+import javax.sql.DataSource;
|
|
|
+import java.io.*;
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.sql.Connection;
|
|
|
+import java.sql.SQLException;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class DatabaseBackupRestoreService {
|
|
|
+
|
|
|
+ @Value("${spring.datasource.dynamic.datasource.master.url}")
|
|
|
+ private String url;
|
|
|
+ @Value("${spring.datasource.dynamic.datasource.master.username}")
|
|
|
+ private String username;
|
|
|
+ @Value("${spring.datasource.dynamic.datasource.master.password}")
|
|
|
+ private String password;
|
|
|
+ @Autowired
|
|
|
+ private DataSource dataSource;
|
|
|
+ private String getDatabaseName() {
|
|
|
+ return url.split("/")[3].split("\\?")[0];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 备份数据库并写入 HttpServletResponse,触发文件下载
|
|
|
+ */
|
|
|
+ public void backupDatabaseToResponse(HttpServletResponse response) throws IOException {
|
|
|
+ String dbName = getDatabaseName();
|
|
|
+
|
|
|
+ // 生成文件名
|
|
|
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
|
|
+ String filename = "db_backup_" + LocalDateTime.now().format(formatter) + ".sql";
|
|
|
+
|
|
|
+ response.reset();
|
|
|
+ response.setContentType("application/octet-stream");
|
|
|
+ String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
|
|
|
+ response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFilename);
|
|
|
+
|
|
|
+ ProcessBuilder pb = new ProcessBuilder(
|
|
|
+ "mysqldump",
|
|
|
+ "-u" + username,
|
|
|
+ "--password=" + password,
|
|
|
+ "--replace",
|
|
|
+ dbName
|
|
|
+ );
|
|
|
+ pb.redirectErrorStream(true);
|
|
|
+
|
|
|
+ Process process;
|
|
|
+ try {
|
|
|
+ process = pb.start();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("启动 mysqldump 失败", e);
|
|
|
+ throw new IOException("无法启动备份进程: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+
|
|
|
+ try (OutputStream out = response.getOutputStream();
|
|
|
+ InputStream in = process.getInputStream()) {
|
|
|
+
|
|
|
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
|
|
+ byte[] data = new byte[4096];
|
|
|
+ int len, total = 0;
|
|
|
+
|
|
|
+ while ((len = in.read(data)) != -1) {
|
|
|
+ buffer.write(data, 0, len);
|
|
|
+ out.write(data, 0, len);
|
|
|
+ total += len;
|
|
|
+ }
|
|
|
+ int exitCode = process.waitFor();
|
|
|
+ if (exitCode != 0) {
|
|
|
+ String errorOutput = buffer.toString(StandardCharsets.UTF_8);
|
|
|
+ log.error("mysqldump 失败,退出码: {}, 错误: {}", exitCode, errorOutput);
|
|
|
+ throw new RuntimeException("备份失败: " + errorOutput);
|
|
|
+ }
|
|
|
+ out.flush();
|
|
|
+
|
|
|
+ log.info("备份完成,共输出 {} 字节,文件名: {}", total, filename);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("备份失败", e);
|
|
|
+ throw new IOException("备份失败: " + e.getMessage(), e);
|
|
|
+ } finally {
|
|
|
+ process.destroyForcibly();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从 MultipartFile 恢复数据库
|
|
|
+ */
|
|
|
+
|
|
|
+ public String restoreFromMultipartFile(MultipartFile file) throws IOException {
|
|
|
+ try (
|
|
|
+ Connection conn = dataSource.getConnection();
|
|
|
+ InputStream rawInputStream = file.getInputStream();
|
|
|
+ InputStream filteredStream = new FilteredCommentInputStream(rawInputStream, StandardCharsets.UTF_8);
|
|
|
+ InputStreamReader reader = new InputStreamReader(filteredStream, StandardCharsets.UTF_8)
|
|
|
+ ) {
|
|
|
+ boolean autoCommit = conn.getAutoCommit();
|
|
|
+ conn.setAutoCommit(false);
|
|
|
+
|
|
|
+ try {
|
|
|
+ ScriptRunner scriptRunner = new ScriptRunner(conn);
|
|
|
+ scriptRunner.setLogWriter(null); // 不打印每条 SQL
|
|
|
+ scriptRunner.setStopOnError(true); // 遇错停止
|
|
|
+ scriptRunner.setEscapeProcessing(false); // 提高性能,如果你的 SQL 不含 \n \t 转义
|
|
|
+
|
|
|
+ scriptRunner.runScript(reader);
|
|
|
+
|
|
|
+ conn.commit();
|
|
|
+ return "恢复成功";
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ try {
|
|
|
+ conn.rollback();
|
|
|
+ } catch (SQLException rollbackEx) {
|
|
|
+ throw new IOException("恢复失败且回滚异常: " + rollbackEx.getMessage(), rollbackEx);
|
|
|
+ }
|
|
|
+ throw new IOException("SQL 脚本执行失败: " + e.getMessage(), e);
|
|
|
+ } finally {
|
|
|
+ try {
|
|
|
+ conn.setAutoCommit(autoCommit);
|
|
|
+ } catch (SQLException e) {
|
|
|
+ log.warn("恢复 AutoCommit 状态失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (SQLException e) {
|
|
|
+ throw new IOException("数据库连接失败: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 执行系统命令
|
|
|
+ */
|
|
|
+ private String executeCommand(String command, String operation) {
|
|
|
+ try {
|
|
|
+ ProcessBuilder processBuilder = new ProcessBuilder("bash", "-c", command);
|
|
|
+ processBuilder.redirectErrorStream(true);
|
|
|
+
|
|
|
+ Process process = processBuilder.start();
|
|
|
+
|
|
|
+ BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
|
|
+ StringBuilder output = new StringBuilder();
|
|
|
+ String line;
|
|
|
+ while ((line = reader.readLine()) != null) {
|
|
|
+ output.append(line).append("\n");
|
|
|
+ }
|
|
|
+
|
|
|
+ int exitCode = process.waitFor();
|
|
|
+ if (exitCode == 0) {
|
|
|
+ return operation + "成功: " + output.toString();
|
|
|
+ } else {
|
|
|
+ return operation + "失败,退出码: " + exitCode + ",输出: " + output.toString();
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error(operation + "异常", e);
|
|
|
+ return operation + "异常: " + e.getMessage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|