using AIK.Common; using AIK.Common.WebHelper; using AIK.Service.Extensions; using AIK.Service.IService; using Microsoft.Extensions.Logging; using SharpCompress.Common; using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; namespace AIK.Service.Service { public class FileCloudDataService : IFileCacheService { private readonly string _cacheBaseDirectory; private readonly IHttpClientService _httpClient; private readonly IConfigurationService _configurationService; private readonly ILogger _logger; private string GetCacheFilePath(string cacheKey) => Path.Combine(_cacheBaseDirectory, $"{cacheKey}"); public FileCloudDataService(IConfigurationService configurationService, IHttpClientService httpClient, ILogger logger) { _configurationService = configurationService; _httpClient = httpClient; _logger = logger; // 设置缓存目录,例如在用户本地应用数据文件夹下 _cacheBaseDirectory = CommonString.CloudCacheDirectory; Directory.CreateDirectory(_cacheBaseDirectory); // 确保目录存在 } public async Task ClearAllCacheAsync() { await Task.Run(() => { try { var directory = new DirectoryInfo(_cacheBaseDirectory); if (directory.Exists) { directory.Delete(true); Directory.CreateDirectory(_cacheBaseDirectory); _logger.LogInformation("清空所有缓存完成"); } } catch (Exception ex) { _logger.LogError(ex, "清空所有缓存失败"); } }); } public async Task ClearExpiredCacheAsync() { await Task.Run(() => { try { var directory = new DirectoryInfo(_cacheBaseDirectory); if (!directory.Exists) return; int deletedCount = 0; foreach (var file in directory.GetFiles("*.bin")) { try { file.Delete(); deletedCount++; } catch (Exception ex) { _logger.LogWarning(ex, "删除缓存文件失败: {FilePath}", file.FullName); } } _logger.LogInformation("清理缓存完成,删除文件数: {DeletedCount}", deletedCount); } catch (Exception ex) { _logger.LogError(ex, "清理缓存失败"); } }); } public async Task GetFileByteAsync(string fileUrl, string cacheKey = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(fileUrl)) return GetEmptyByte(); // 生成缓存键,如果未提供则使用URL的MD5值 var key = cacheKey ?? GenerateCacheKey(fileUrl); if (fileUrl.Contains("http")) { var urifileName = UrlHelper.GetEncodedFileNameFromUrl(fileUrl); var realfileName = Uri.UnescapeDataString(urifileName); key = realfileName; } // 2. 检查磁盘缓存 string filePath = GetCacheFilePath(key); if (File.Exists(filePath)) { //_logger.LogDebug("磁盘缓存命中: {FilePath}", filePath); // 从磁盘加载并放入内存 var filebyte = await LoadFileFromDiskAsync(filePath); return filebyte; } // 3. 从网络下载 try { var fullImgUrl = fileUrl; if (fileUrl.Contains("http")) { fullImgUrl = fileUrl; } _logger.LogInformation("开始下载bin文件: {Url}", fullImgUrl); // Flurl.Http 字节下载(连接级显式 TLS 1.2;非 2xx 会抛异常,由外层 catch 统一处理) byte[] fileBytes = await _httpClient.GetByteArrayWithTimeoutAsync(fullImgUrl); if (fileBytes == null || fileBytes.Length == 0) { _logger.LogWarning("文件数据为空: {Url}", fullImgUrl); return GetEmptyByte(); } // 保存到磁盘缓存 await SavefileToDiskAsync(fileBytes, filePath); return fileBytes; } catch (Exception ex) { _logger.LogError(ex, "下载文件失败: {Url}", fileUrl); } return GetEmptyByte(); } public async Task<(bool success, string localFilePath)> DownloadFileWithProgressAsync(string fileUrl, string cacheKey, IProgress progress = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(fileUrl)) return (false, string.Empty); // 生成缓存键,如果未提供则使用URL的MD5值 var key = cacheKey ?? GenerateCacheKey(fileUrl); if (fileUrl.Contains("http")) { var urifileName = UrlHelper.GetEncodedFileNameFromUrl(fileUrl); var realfileName = Uri.UnescapeDataString(urifileName); key = realfileName; } // 2. 检查磁盘缓存 string filePath = GetCacheFilePath(key); if (File.Exists(filePath)) { // 校验缓存 zip 有效性:历史版本截断下载可能留下损坏文件,命中坏缓存会导致解压永远失败 if (IsValidZipFile(filePath)) { progress?.Report(100.0); return (true, filePath); } _logger.LogWarning("缓存 zip 文件损坏,删除并重新下载: {FilePath}", filePath); try { File.Delete(filePath); } catch (Exception ex) { _logger.LogWarning(ex, "删除损坏缓存失败: {FilePath}", filePath); } } // 3. 从网络流式下载到临时文件(每段读取空闲超时、无总时长限制;支持断点续传),成功后原子替换 var fullImgUrl = fileUrl; var tempFilePath = filePath + ".tmp"; const int maxAttempts = 5; for (int attempt = 1; attempt <= maxAttempts; attempt++) { try { // 断点续传:检查临时文件已下载的字节数,作为 Range 偏移从断点继续 long resumeOffset = 0; if (File.Exists(tempFilePath)) { resumeOffset = new FileInfo(tempFilePath).Length; } if (resumeOffset > 0) { _logger.LogInformation("断点续传下载: {Url},已有 {Offset} 字节(第 {Attempt}/{Max} 次)", fullImgUrl, resumeOffset, attempt, maxAttempts); } else { _logger.LogInformation("开始下载bin文件: {Url}", fullImgUrl); } // 流式写盘:BouncyCastle TLS + 逐段读取(空闲 60s 超时)+ Range 断点续传 await _httpClient.DownloadToFileWithProgressAsync(fullImgUrl, tempFilePath, progress, cancellationToken, resumeOffset); // 下载产物完整性校验(拦截截断/错误页),不通过则抛异常走重试 if (!IsValidZipFile(tempFilePath)) { throw new InvalidDataException($"下载的 zip 文件无效(可能被截断): {fullImgUrl}"); } // 原子性操作:临时文件替换正式文件 if (File.Exists(filePath)) { File.Delete(filePath); } File.Move(tempFilePath, filePath); progress?.Report(100.0); return (true, filePath); } catch (Exception ex) { // 保留本次已下载的临时文件(供下次断点续传),仅在最终失败时清理 if (attempt < maxAttempts && !cancellationToken.IsCancellationRequested) { _logger.LogWarning(ex, "下载文件失败(第 {Attempt}/{Max} 次,将断点续传): {Url}", attempt, maxAttempts, fullImgUrl); await Task.Delay(TimeSpan.FromSeconds(attempt), cancellationToken).ConfigureAwait(false); } else { _logger.LogError(ex, "下载文件失败(最终): {Url}", fullImgUrl); try { if (File.Exists(tempFilePath)) { File.Delete(tempFilePath); } } catch { } return (false, string.Empty); } } } return (false, string.Empty); } #region 辅助方法 private byte[] GetEmptyByte() { return Array.Empty(); } /// /// 保存字节数组到磁盘 /// private async Task SavefileToDiskAsync(byte[] fileBytes, string filePath) { try { // 使用临时文件避免写入过程中出错 var tempFilePath = filePath + ".tmp"; File.WriteAllBytes(tempFilePath, fileBytes); // 原子性操作:重命名临时文件 if (File.Exists(filePath)) { File.Delete(filePath); } File.Move(tempFilePath, filePath); _logger.LogDebug("文件保存成功: {FilePath}, 大小: {Size}字节", filePath, fileBytes.Length); } catch (Exception ex) { _logger.LogError(ex, "保存文件到磁盘失败: {FilePath}", filePath); // 清理临时文件 var tempFilePath = filePath + ".tmp"; if (File.Exists(tempFilePath)) { File.Delete(tempFilePath); } } } /// /// 优化的磁盘加载方法 /// private async Task LoadFileFromDiskAsync(string filePath) { if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) { return GetEmptyByte(); } try { // 直接读取文件字节数组 byte[] fileBytes = File.ReadAllBytes(filePath); if (fileBytes == null || fileBytes.Length == 0) { _logger.LogWarning("磁盘缓存文件为空: {FilePath}", filePath); // 删除损坏的文件 File.Delete(filePath); return GetEmptyByte(); } return fileBytes; } catch (Exception ex) { _logger.LogError(ex, "从磁盘加载文件失败: {FilePath}", filePath); return GetEmptyByte(); } } private string GenerateCacheKey(string url) { using (var md5 = System.Security.Cryptography.MD5.Create()) { byte[] inputBytes = System.Text.Encoding.UTF8.GetBytes(url); byte[] hashBytes = md5.ComputeHash(inputBytes); return Convert.ToBase64String(hashBytes).Replace("/", "_").Replace("+", "-").Substring(0, 10); } } /// /// 校验 zip 文件是否有效(能否打开中央目录记录)。 /// 用于拦截:历史版本截断下载留下的损坏缓存、本次下载被中途掐断或返回错误页的产物。 /// private static bool IsValidZipFile(string filePath) { try { using (var archive = ZipFile.OpenRead(filePath)) { return archive.Entries.Count > 0 || new FileInfo(filePath).Length > 0; } } catch (Exception ex) when (ex is InvalidDataException || ex is IOException || ex is UnauthorizedAccessException) { return false; } catch { return false; } } #endregion } }