diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..fe1152b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,30 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md
+!**/.gitignore
+!.git/HEAD
+!.git/config
+!.git/packed-refs
+!.git/refs/heads/**
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 9491a2f..b41384d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -360,4 +360,6 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
-FodyWeavers.xsd
\ No newline at end of file
+FodyWeavers.xsd
+/SQLBackupToCOS/appsettings.json
+/SQLBackupToCOS/appsettings.json
diff --git a/SQLBackupToCOS.slnx b/SQLBackupToCOS.slnx
new file mode 100644
index 0000000..baa69e5
--- /dev/null
+++ b/SQLBackupToCOS.slnx
@@ -0,0 +1,3 @@
+
+
+
diff --git a/SQLBackupToCOS/BackupService.cs b/SQLBackupToCOS/BackupService.cs
new file mode 100644
index 0000000..968753f
--- /dev/null
+++ b/SQLBackupToCOS/BackupService.cs
@@ -0,0 +1,145 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using SQLBackupToCOS;
+using System.Diagnostics;
+using System.Text;
+
+public class BackupService(ILogger logger, IConfiguration config, COSService cosService) : BackgroundService
+{
+ private readonly ILogger _logger = logger;
+ private readonly IConfiguration _config = config;
+ private readonly COSService _cosService = cosService;
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ var interval = TimeSpan.FromMinutes(_config.GetValue("BackupIntervalMinutes", 60));
+ using var timer = new PeriodicTimer(interval);
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ await RunOnceAsync(stoppingToken);
+ }
+ catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
+ {
+ _logger.LogInformation("Cancellation requested.");
+ break;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Backup failed.");
+ }
+
+ if (!await timer.WaitForNextTickAsync(stoppingToken))
+ break;
+ }
+ }
+
+ private async Task RunOnceAsync(CancellationToken ct)
+ {
+ var url = _config.GetValue("Database:Host") ?? "172.17.0.1";
+ var user = _config.GetValue("Database:User") ?? "backupUser";
+ var password = _config.GetValue("Database:Password") ?? "";
+ var databases = _config.GetSection("Database:Databases").Get() ?? ["my_database"];
+
+ var timestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
+ var dumpDir = $"/data/dumps/dump-{timestamp}";
+ var finalDump = $"/data/dumps/dump-{timestamp}.tar.gz";
+
+ try
+ {
+ Directory.CreateDirectory(dumpDir);
+
+ foreach (var database in databases)
+ {
+ var dumpFile = Path.Combine(dumpDir, $"{database}.sql");
+ _logger.LogInformation("Starting backup for database: {Database}", database);
+
+ var psi = new ProcessStartInfo
+ {
+ FileName = "mysqldump",
+ Arguments = $"-h {url} -u{user} --single-transaction --quick --routines --triggers --events {database}",
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ };
+
+ psi.Environment["MYSQL_PWD"] = password;
+
+ using var proc = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start mysqldump");
+ await using var fs = new FileStream(dumpFile, FileMode.CreateNew, FileAccess.Write, FileShare.None);
+
+ var copyTask = proc.StandardOutput.BaseStream.CopyToAsync(fs, 81920, ct);
+ var stderrTask = ConsumeStreamToLoggerAsync(proc.StandardError, _logger, ct);
+
+ await Task.WhenAll(copyTask, stderrTask);
+ await proc.WaitForExitAsync(ct);
+
+ if (proc.ExitCode != 0)
+ {
+ _logger.LogWarning("mysqldump for {Database} exited with code {Code}", database, proc.ExitCode);
+ }
+ else
+ {
+ _logger.LogInformation("Backup created for {Database}: {File}", database, dumpFile);
+ }
+ }
+
+ _logger.LogInformation("Compressing backup directory to {File}", finalDump);
+ await CompressDirectoryAsync(dumpDir, finalDump, ct);
+
+ Directory.Delete(dumpDir, recursive: true);
+ _logger.LogInformation("Backup completed: {File}", finalDump);
+
+ await _cosService.AddFileToCOS(finalDump);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error during backup process.");
+ throw;
+ }
+ }
+
+ private async Task CompressDirectoryAsync(string sourceDir, string targetFile, CancellationToken ct)
+ {
+ // 使用 tar + gzip 压缩目录
+ var psi = new ProcessStartInfo
+ {
+ FileName = "tar",
+ Arguments = $"-czf {targetFile} -C {Path.GetDirectoryName(sourceDir)} {Path.GetFileName(sourceDir)}",
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true,
+ };
+
+ using var proc = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start tar");
+ var stderrTask = ConsumeStreamToLoggerAsync(proc.StandardError, _logger, ct);
+ await stderrTask;
+ await proc.WaitForExitAsync(ct);
+
+ if (proc.ExitCode != 0)
+ {
+ throw new InvalidOperationException($"tar exited with code {proc.ExitCode}");
+ }
+ }
+
+ private async Task ConsumeStreamToLoggerAsync(StreamReader err, ILogger log, CancellationToken ct)
+ {
+ var sb = new StringBuilder();
+ char[] buffer = new char[4096];
+ int read;
+ while ((read = await err.ReadAsync(buffer, 0, buffer.Length)) > 0)
+ {
+ sb.Append(buffer, 0, read);
+ if (sb.Length > 1024)
+ {
+ log.LogWarning("mysqldump stderr: {text}", sb.ToString());
+ sb.Clear();
+ }
+ }
+ if (sb.Length > 0) log.LogWarning("mysqldump stderr final: {text}", sb.ToString());
+ }
+}
\ No newline at end of file
diff --git a/SQLBackupToCOS/COSService.cs b/SQLBackupToCOS/COSService.cs
new file mode 100644
index 0000000..edd0346
--- /dev/null
+++ b/SQLBackupToCOS/COSService.cs
@@ -0,0 +1,59 @@
+using COSXML;
+using COSXML.Auth;
+using COSXML.Model;
+using COSXML.Transfer;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace SQLBackupToCOS
+{
+ public class COSService
+ {
+ private readonly IConfiguration _config;
+ private readonly ILogger _logger;
+
+ public COSService(IConfiguration config, ILogger logger)
+ {
+ _config = config;
+ _logger = logger;
+ }
+ public async Task AddFileToCOS(string srcPath)
+ {
+ var bucketName = _config.GetValue("COS:BucketName") ?? "my-bucket";
+ var region = _config.GetValue("COS:Region") ?? "ap-shanghai";
+ var secretId = _config.GetValue("COS:SecretId") ?? "";
+ var secretKey = _config.GetValue("COS:SecretKey") ?? "";
+ var filePath = _config.GetValue("COS:FilePath") ?? "/data/dumps/";
+ CosXmlConfig config = new CosXmlConfig.Builder()
+ .IsHttps(true) // 设置默认 HTTPS 请求
+ .SetRegion(region) // 设置一个默认的存储桶地域
+ .Build();
+ QCloudCredentialProvider cosCredentialProvider = new DefaultQCloudCredentialProvider(secretId, secretKey, 600);
+ var cosXML = new CosXmlServer(config, cosCredentialProvider);
+ // 初始化 TransferConfig
+ TransferConfig transferConfig = new TransferConfig();
+
+ // 初始化 TransferManager
+ TransferManager transferManager = new TransferManager(cosXML, transferConfig);
+
+ string cosPath = Path.Combine(filePath, Path.GetFileName(srcPath));
+ // 上传对象
+ COSXMLUploadTask uploadTask = new COSXMLUploadTask(bucketName, cosPath);
+ uploadTask.SetSrcPath(srcPath);
+
+ uploadTask.successCallback = delegate (CosResult result)
+ {
+ _logger.LogInformation("Upload Success: " + result.GetResultInfo());
+ };
+
+ try
+ {
+ await transferManager.UploadAsync(uploadTask);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError("CosException: " + e);
+ }
+ }
+ }
+}
diff --git a/SQLBackupToCOS/Dockerfile b/SQLBackupToCOS/Dockerfile
new file mode 100644
index 0000000..7f7c50e
--- /dev/null
+++ b/SQLBackupToCOS/Dockerfile
@@ -0,0 +1,33 @@
+# 请参阅 https://aka.ms/customizecontainer 以了解如何自定义调试容器,以及 Visual Studio 如何使用此 Dockerfile 生成映像以更快地进行调试。
+
+# 此阶段用于在快速模式(默认为调试配置)下从 VS 运行时
+FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base
+
+RUN apt update && apt install -y mysql-client && mkdir /data && chmod 777 /data
+ENV TZ="Asia/Shanghai"
+USER $APP_UID
+WORKDIR /app
+
+
+# 此阶段用于生成服务项目
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["SQLBackupToCOS/SQLBackupToCOS.csproj", "SQLBackupToCOS/"]
+RUN dotnet restore "./SQLBackupToCOS/SQLBackupToCOS.csproj"
+COPY . .
+WORKDIR "/src/SQLBackupToCOS"
+RUN dotnet build "./SQLBackupToCOS.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+# 此阶段用于发布要复制到最终阶段的服务项目
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./SQLBackupToCOS.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+# 此阶段在生产中使用,或在常规模式下从 VS 运行时使用(在不使用调试配置时为默认值)
+FROM base AS final
+WORKDIR /app
+ENV TZ="Asia/Shanghai"
+RUN apt update && apt install -y mysql-client && mkdir /data && chmod 777 /data
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "SQLBackupToCOS.dll"]
\ No newline at end of file
diff --git a/SQLBackupToCOS/Program.cs b/SQLBackupToCOS/Program.cs
new file mode 100644
index 0000000..bb73d79
--- /dev/null
+++ b/SQLBackupToCOS/Program.cs
@@ -0,0 +1,14 @@
+// Program.cs (示例核心片段)
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using SQLBackupToCOS;
+
+var builder = Host.CreateDefaultBuilder(args)
+ .ConfigureServices(services =>
+ {
+ services.AddLogging();
+ services.AddSingleton();
+ services.AddHostedService();
+ });
+
+await builder.RunConsoleAsync();
diff --git a/SQLBackupToCOS/Properties/launchSettings.json b/SQLBackupToCOS/Properties/launchSettings.json
new file mode 100644
index 0000000..74429d6
--- /dev/null
+++ b/SQLBackupToCOS/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "SQLBackupToCOS": {
+ "commandName": "Project"
+ },
+ "Container (Dockerfile)": {
+ "commandName": "Docker"
+ }
+ }
+}
\ No newline at end of file
diff --git a/SQLBackupToCOS/SQLBackupToCOS.csproj b/SQLBackupToCOS/SQLBackupToCOS.csproj
new file mode 100644
index 0000000..4ba4115
--- /dev/null
+++ b/SQLBackupToCOS/SQLBackupToCOS.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ Linux
+
+
+
+
+
+
+
+
+
+
diff --git a/SQLBackupToCOS/appsettings.json.sample b/SQLBackupToCOS/appsettings.json.sample
new file mode 100644
index 0000000..e634b97
--- /dev/null
+++ b/SQLBackupToCOS/appsettings.json.sample
@@ -0,0 +1,22 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "BackupIntervalMinutes": 60,
+ "Database": {
+ "Host": "172.17.0.1",
+ "User": "SQLUSER",
+ "Password": "SQLPASSWORD",
+ "Databases": [ "DB1", "DB2" ]
+ },
+ "COS": {
+ "BucketName": "BUCKETNAME",
+ "Region": "BUCKETREGION",
+ "SecretId": "ID",
+ "SecretKey": "KEY",
+ "FilePath": "PATH"
+ }
+}