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" + } +}