From 5f3f3fd43ec2182a9bb26889298134bd2e4db105 Mon Sep 17 00:00:00 2001 From: li-chx Date: Thu, 20 Nov 2025 09:27:51 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E4=BD=BF=E7=94=A8LH-COS=E4=BB=A3?= =?UTF-8?q?=E6=9B=BFCOS=20:poop:=20=E6=9C=AA=E7=BB=8F=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SQLBackupToCOS/BackupService.cs | 94 +++++++++++++++++++++++++- SQLBackupToCOS/COSService.cs | 12 ++-- SQLBackupToCOS/OutputService.cs | 52 ++++++++++++++ SQLBackupToCOS/Program.cs | 3 +- SQLBackupToCOS/appsettings.json | 3 + SQLBackupToCOS/appsettings.json.sample | 2 + docker-compose.yml | 3 + 7 files changed, 157 insertions(+), 12 deletions(-) create mode 100644 SQLBackupToCOS/OutputService.cs diff --git a/SQLBackupToCOS/BackupService.cs b/SQLBackupToCOS/BackupService.cs index ae4ead4..58995ef 100644 --- a/SQLBackupToCOS/BackupService.cs +++ b/SQLBackupToCOS/BackupService.cs @@ -6,14 +6,101 @@ using System.Text; namespace SQLBackupToCOS; -public class BackupService(ILogger logger, IConfiguration config, COSService cosService) : BackgroundService +public class BackupService(ILogger logger, IConfiguration config, OutputService outputService) : BackgroundService { private readonly ILogger _logger = logger; private readonly IConfiguration _config = config; - private readonly COSService _cosService = cosService; + //private readonly COSService _cosService = cosService; + private readonly OutputService _outputService = outputService; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + var startTime = _config.GetValue("startedAt"); + if (!string.IsNullOrWhiteSpace(startTime)) + { + DateTime scheduledTime; + + if (TimeOnly.TryParse(startTime, out var timeOnly)) + { + // 只有时间(如 "08:00:00" 或 "08:00") + // 计算今天的目标时间(UTC+8) + TimeZoneInfo timeZone; + try + { + // 尝试使用跨平台的时区 ID + timeZone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai"); + } + catch (TimeZoneNotFoundException) + { + // 回退到 Windows 时区 ID + timeZone = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"); + } + + var nowUtc8 = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, timeZone); + + // 构建今天的目标时间 + scheduledTime = new DateTime( + nowUtc8.Year, + nowUtc8.Month, + nowUtc8.Day, + timeOnly.Hour, + timeOnly.Minute, + timeOnly.Second); + + // 如果已经过了今天的时间,则安排到明天 + if (scheduledTime <= nowUtc8) + { + scheduledTime = scheduledTime.AddDays(1); + _logger.LogInformation("Scheduled time already passed today, scheduling for tomorrow: {Time}", scheduledTime); + } + + // 转换回 UTC 以便计算延迟 + var scheduledTimeUtc = TimeZoneInfo.ConvertTimeToUtc(scheduledTime, timeZone); + var delay = scheduledTimeUtc - DateTime.UtcNow; + + if (delay > TimeSpan.Zero) + { + _logger.LogInformation("Waiting until scheduled start time: {Time} UTC+8 (in {Delay})", scheduledTime, delay); + try + { + await Task.Delay(delay, stoppingToken); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + _logger.LogInformation("Cancellation requested before start."); + return; + } + } + } + else if (DateTime.TryParse(startTime, out scheduledTime)) + { + // 完整日期时间(如 "2024-01-15 08:00:00") + var delay = scheduledTime - DateTime.Now; + + if (delay > TimeSpan.Zero) + { + _logger.LogInformation("Waiting until scheduled start time: {Time} (in {Delay})", scheduledTime, delay); + try + { + await Task.Delay(delay, stoppingToken); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + _logger.LogInformation("Cancellation requested before start."); + return; + } + } + } + else + { + _logger.LogWarning("Invalid scheduled start time format: {Time}, starting immediately.", startTime); + } + } + else + { + _logger.LogInformation("No valid scheduled start time provided, starting immediately."); + } + var interval = TimeSpan.FromMinutes(_config.GetValue("BackupIntervalMinutes", 60)); using var timer = new PeriodicTimer(interval); @@ -98,7 +185,8 @@ public class BackupService(ILogger logger, IConfiguration config, Directory.Delete(dumpDir, recursive: true); _logger.LogInformation("Backup completed: {File}", finalDump); - await _cosService.AddFileToCOS(finalDump); + await _outputService.AddFileToOutput(finalDump); + //await _cosService.AddFileToCOS(finalDump); } catch (Exception ex) { diff --git a/SQLBackupToCOS/COSService.cs b/SQLBackupToCOS/COSService.cs index edd0346..f8b9388 100644 --- a/SQLBackupToCOS/COSService.cs +++ b/SQLBackupToCOS/COSService.cs @@ -7,16 +7,12 @@ using Microsoft.Extensions.Logging; namespace SQLBackupToCOS { - public class COSService + [Obsolete("This class uses COSXML SDK, now we migrated to use LH-COS")] + public class COSService(IConfiguration config, ILogger logger) { - private readonly IConfiguration _config; - private readonly ILogger _logger; + private readonly IConfiguration _config = config; + private readonly ILogger _logger = 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"; diff --git a/SQLBackupToCOS/OutputService.cs b/SQLBackupToCOS/OutputService.cs new file mode 100644 index 0000000..f3f4e9c --- /dev/null +++ b/SQLBackupToCOS/OutputService.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace SQLBackupToCOS +{ + public class OutputService(IConfiguration config, ILogger logger) + { + private readonly IConfiguration _config = config; + private readonly ILogger _logger = logger; + + public async Task AddFileToOutput(string srcPath) + { + try + { + string outputPath = _config.GetValue("outputDir") ?? "/output"; + Directory.CreateDirectory(outputPath); + + // 构建目标文件完整路径 + var fileName = Path.GetFileName(srcPath); + var destFilePath = Path.Combine(outputPath, fileName); + + _logger.LogInformation("Copying backup file from {Source} to {Destination}", srcPath, destFilePath); + + // 异步复制文件 + await using var sourceStream = new FileStream( + srcPath, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + 81920, + FileOptions.Asynchronous); + + await using var destStream = new FileStream( + destFilePath, + FileMode.Create, + FileAccess.Write, + FileShare.None, + 81920, + FileOptions.Asynchronous); + + await sourceStream.CopyToAsync(destStream); + + _logger.LogInformation("Backup file copied successfully to {Destination}", destFilePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to copy backup file to output directory"); + throw; + } + } + } +} diff --git a/SQLBackupToCOS/Program.cs b/SQLBackupToCOS/Program.cs index bb73d79..70b6300 100644 --- a/SQLBackupToCOS/Program.cs +++ b/SQLBackupToCOS/Program.cs @@ -7,7 +7,8 @@ var builder = Host.CreateDefaultBuilder(args) .ConfigureServices(services => { services.AddLogging(); - services.AddSingleton(); + //services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(); }); diff --git a/SQLBackupToCOS/appsettings.json b/SQLBackupToCOS/appsettings.json index b565d1e..da68506 100644 --- a/SQLBackupToCOS/appsettings.json +++ b/SQLBackupToCOS/appsettings.json @@ -6,6 +6,9 @@ } }, "BackupIntervalMinutes": 60, + "extraDir": "", + "outputDir": "", + "startedAt": "", "Database": { "Host": "", "User": "", diff --git a/SQLBackupToCOS/appsettings.json.sample b/SQLBackupToCOS/appsettings.json.sample index fe4fdd4..ba23856 100644 --- a/SQLBackupToCOS/appsettings.json.sample +++ b/SQLBackupToCOS/appsettings.json.sample @@ -7,6 +7,8 @@ }, "BackupIntervalMinutes": 60, "extraDir": "EXTRAPATH", + "outputDir": "OUTPUTPATH", + "startedAt": "STARTTIME", "Database": { "Host": "172.17.0.1", "User": "SQLUSER", diff --git a/docker-compose.yml b/docker-compose.yml index d003016..8d1cb45 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,10 @@ services: COS__SecretId: SECRETID COS__SecretKey: SECRETKEY COS__FilePath: PATH + startedAt: '03:00:00' BackupIntervalMinutes: 1440 extraDir: EXTRAPATH + outputDir: OUTPUTPATH volumes: - srcDir:EXTRAPATH/srcDir:ro + - outputDir:OUTPUTPAPH:rw