using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System.Text; using WIDESEAWCS_Core; using WIDESEAWCS_ISystemServices; using WIDESEAWCS_Model.Models; namespace WIDESEAWMSServer.Controllers { [Route("api/Sys_Log")] [ApiController] public class Sys_LogController : ApiBaseController { private const int MAX_FILE_SIZE_MB = 50; private const int MAX_RETRY_COUNT = 3; private const int RETRY_DELAY_MS = 100; private static readonly string[] ALLOWED_FILE_TYPES = { ".txt", ".log", ".csv", ".json", ".xml" }; private readonly IHttpContextAccessor _httpContextAccessor; public Sys_LogController(ISys_LogService service, IHttpContextAccessor httpContextAccessor) : base(service) { _httpContextAccessor = httpContextAccessor; } [HttpPost, Route("GetLogList"), AllowAnonymous] public WebResponseContent GetLogList() { return Service.GetLogList(); } [HttpPost, Route("GetLogData"), AllowAnonymous] public WebResponseContent GetLogData([FromBody] GetLogParm parm) { return Service.GetLogData(parm); } [HttpPost, Route("GetLogName"), AllowAnonymous] public WebResponseContent GetLogName() { return Service.GetLogName(); } [HttpPost, Route("GetLog"), AllowAnonymous] public WebResponseContent GetLog(string fileName) { return Service.GetLog(fileName); } [HttpPost, HttpGet, Route("DownLoadLog"), AllowAnonymous] public virtual async Task DownLoadLog(string fileName) { try { // 1. 参数验证 if (string.IsNullOrWhiteSpace(fileName)) { return BadRequest("文件名不能为空"); } // 安全性检查:防止路径遍历攻击 if (fileName.Contains("..") || Path.IsPathRooted(fileName)) { return BadRequest("无效的文件名"); } //string logDirectory = Path.Combine(AppContext.BaseDirectory, "logs"); string logDirectory = Path.Combine(AppContext.BaseDirectory); if (!Directory.Exists(logDirectory)) { Directory.CreateDirectory(logDirectory); } string filePath = Path.Combine(logDirectory, fileName); if (Directory.Exists(filePath)) { return NotFound($"文件 {fileName} 不存在"); } string extension = Path.GetExtension(fileName).ToLowerInvariant(); if (!IsAllowedFileType(extension)) { return BadRequest($"不支持的文件类型: {extension}"); } FileInfo fileInfo = new FileInfo(filePath); if (fileInfo.Length > MAX_FILE_SIZE_MB * 1024 * 1024) { return BadRequest($"文件过大,超过{MAX_FILE_SIZE_MB}MB限制"); } // 方案1:使用重试机制 + 共享读取(推荐) byte[] fileBytes = await ReadFileWithRetryAsync(filePath); if (fileBytes == null) { return StatusCode(500, "文件被占用,无法下载,请稍后重试"); } string contentType = GetContentType(extension); // 设置下载头 Response.Headers.Add("Content-Disposition", $"attachment; filename=\"{System.Web.HttpUtility.UrlEncode(fileName, Encoding.UTF8)}\""); Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); Response.Headers.Add("Pragma", "no-cache"); Response.Headers.Add("Expires", "0"); return File(fileBytes, contentType, fileName); } catch (UnauthorizedAccessException) { return StatusCode(403, "没有访问该文件的权限"); } catch (PathTooLongException) { return BadRequest("文件路径过长"); } catch (IOException ex) { if (IsFileLockedException(ex)) { return StatusCode(500, "文件被锁定,可能正在被系统写入,请稍后再试"); } return StatusCode(500, $"文件读取失败: {ex.Message}"); } catch (Exception ex) { // 记录异常日志(这里简化为返回,实际项目中应该记录到日志系统) return StatusCode(500, $"服务器内部错误: {ex.Message}"); } } /// /// 带重试机制的文件读取方法 /// private async Task ReadFileWithRetryAsync(string filePath) { for (int attempt = 0; attempt < MAX_RETRY_COUNT; attempt++) { try { // 使用 FileShare.ReadWrite 允许其他进程同时读取和写入 // 使用异步读取提高性能 using (var fileStream = new FileStream( filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, // 允许删除操作 bufferSize: 4096, useAsync: true)) { using (var memoryStream = new MemoryStream()) { await fileStream.CopyToAsync(memoryStream); return memoryStream.ToArray(); } } } catch (IOException) when (attempt < MAX_RETRY_COUNT - 1) { // 如果不是最后一次重试,等待一段时间 await Task.Delay(RETRY_DELAY_MS * (attempt + 1)); } catch (IOException ex) { // 最后一次尝试也失败了 throw; } } return null; } /// /// 判断是否为文件被锁定的异常 /// private bool IsFileLockedException(IOException ex) { int errorCode = ex.HResult & 0xFFFF; return errorCode == 32 || errorCode == 33; // ERROR_SHARING_VIOLATION or ERROR_LOCK_VIOLATION } /// /// 检查文件类型是否允许 /// private bool IsAllowedFileType(string extension) { return ALLOWED_FILE_TYPES.Contains(extension); } /// /// 获取Content-Type /// private string GetContentType(string extension) { return extension.ToLowerInvariant() switch { ".txt" => "text/plain; charset=utf-8", ".log" => "text/plain; charset=utf-8", ".csv" => "text/csv; charset=utf-8", ".json" => "application/json; charset=utf-8", ".xml" => "application/xml; charset=utf-8", _ => "application/octet-stream" }; } /// /// 备选方案:创建临时副本下载(最安全,但性能稍差) /// [HttpPost, HttpGet, Route("DownLoadLogCopy"), AllowAnonymous] public virtual async Task DownLoadLogCopy(string fileName) { try { // 参数验证(同上) if (string.IsNullOrWhiteSpace(fileName)) { return BadRequest("文件名不能为空"); } string logDirectory = Path.Combine(AppContext.BaseDirectory, "logs"); string filePath = Path.Combine(logDirectory, fileName); if (Directory.Exists(filePath)) { return NotFound($"文件 {fileName} 不存在"); } // 生成临时文件名 string tempFileName = $"{Path.GetFileNameWithoutExtension(fileName)}_{Guid.NewGuid():N}{Path.GetExtension(fileName)}"; string tempFilePath = Path.Combine(Path.GetTempPath(), tempFileName); try { // 尝试复制文件到临时位置(使用重试机制) bool copySuccess = false; for (int attempt = 0; attempt < MAX_RETRY_COUNT; attempt++) { try { //Directory.GetFiles.Copy(filePath, tempFilePath, false); copySuccess = true; break; } catch (IOException) when (attempt < MAX_RETRY_COUNT - 1) { await Task.Delay(RETRY_DELAY_MS * (attempt + 1)); } } if (!copySuccess) { return StatusCode(500, "无法复制文件,可能被其他进程占用"); } // 从临时文件读取 byte[] fileBytes; using (FileStream tempStream = new FileStream(tempFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { using (MemoryStream memoryStream = new MemoryStream()) { await tempStream.CopyToAsync(memoryStream); fileBytes = memoryStream.ToArray(); } } string extension = Path.GetExtension(fileName).ToLowerInvariant(); string contentType = GetContentType(extension); // 返回文件后清理临时文件 var result = File(fileBytes, contentType, fileName); // 异步清理临时文件 _ = Task.Run(() => { try { Directory.Delete(tempFilePath); } catch { // 忽略删除失败 } }); return result; } finally { // 确保临时文件被清理 if (Directory.Exists(tempFilePath)) { try { Directory.Delete(tempFilePath); } catch { // 忽略删除失败 } } } } catch (Exception ex) { return StatusCode(500, $"服务器内部错误: {ex.Message}"); } } } }