| | |
| | | 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 WIDESEAWCS_Server.Controllers.System |
| | | namespace WIDESEAWMSServer.Controllers |
| | | { |
| | | [Route("api/Sys_Log")] |
| | | [ApiController] |
| | | public class Sys_LogController : ApiBaseController<ISys_LogService, Sys_Log> |
| | | { |
| | | 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.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<ActionResult> 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}"); |
| | | } |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 带重试机制的文件读取方法 |
| | | /// </summary> |
| | | private async Task<byte[]> 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; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 判断是否为文件被锁定的异常 |
| | | /// </summary> |
| | | private bool IsFileLockedException(IOException ex) |
| | | { |
| | | int errorCode = ex.HResult & 0xFFFF; |
| | | return errorCode == 32 || errorCode == 33; // ERROR_SHARING_VIOLATION or ERROR_LOCK_VIOLATION |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 检查文件类型是否允许 |
| | | /// </summary> |
| | | private bool IsAllowedFileType(string extension) |
| | | { |
| | | return ALLOWED_FILE_TYPES.Contains(extension); |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 获取Content-Type |
| | | /// </summary> |
| | | 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" |
| | | }; |
| | | } |
| | | |
| | | /// <summary> |
| | | /// 备选方案:创建临时副本下载(最安全,但性能稍差) |
| | | /// </summary> |
| | | [HttpPost, HttpGet, Route("DownLoadLogCopy"), AllowAnonymous] |
| | | public virtual async Task<ActionResult> 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}"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |