| | |
| | | using Microsoft.AspNetCore.Authorization; |
| | | using Autofac.Core; |
| | | using Microsoft.AspNetCore.Authorization; |
| | | using Microsoft.AspNetCore.Http; |
| | | using Microsoft.AspNetCore.Mvc; |
| | | using System.Text; |
| | | using WIDESEAWCS_Core; |
| | | using WIDESEAWCS_Core.BaseController; |
| | | using WIDESEAWCS_DTO.AGV; |
| | | using WIDESEAWCS_ISystemServices; |
| | | using WIDESEAWCS_ITaskInfoService; |
| | | using WIDESEAWCS_Model.Models; |
| | | |
| | | namespace WIDESEAWCS_Server.Controllers.System |
| | | namespace WIDESEAWCSServer.Controllers |
| | | { |
| | | [Route("api/Sys_Log")] |
| | | [ApiController] |
| | | public class Sys_LogController : ApiBaseController<ISys_LogService, Sys_Log> |
| | | { |
| | | private const int MAX_FILE_SIZE_MB = 50; |
| | | private static readonly string[] ALLOWED_FILE_TYPES = { ".txt", ".log", ".csv", ".json", ".xml" }; |
| | | private readonly string[] LogFolders = { "Log", "Log_PLCReadWrite" }; |
| | | 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 |
| | | { |
| | | // 参数验证 |
| | | if (string.IsNullOrWhiteSpace(fileName)) |
| | | { |
| | | return BadRequest("文件名不能为空"); |
| | | } |
| | | |
| | | // 安全性检查 |
| | | if (fileName.Contains("..") || Path.IsPathRooted(fileName) || fileName.Contains("/") || fileName.Contains("\\")) |
| | | { |
| | | return BadRequest("无效的文件名"); |
| | | } |
| | | |
| | | // 检查文件类型 |
| | | string extension = Path.GetExtension(fileName).ToLowerInvariant(); |
| | | if (!IsAllowedFileType(extension)) |
| | | { |
| | | return BadRequest($"不支持的文件类型: {extension}"); |
| | | } |
| | | |
| | | // 查找文件 |
| | | var fileInfo = await FindLogFileAsync(fileName); |
| | | |
| | | if (fileInfo == null) |
| | | { |
| | | return NotFound($"文件 {fileName} 不存在"); |
| | | } |
| | | |
| | | // 检查文件大小(仅用于限制,不加载到内存) |
| | | if (fileInfo.Length > MAX_FILE_SIZE_MB * 1024 * 1024) |
| | | { |
| | | return BadRequest($"文件过大,超过{MAX_FILE_SIZE_MB}MB限制"); |
| | | } |
| | | |
| | | string contentType = GetContentType(extension); |
| | | |
| | | // 设置下载头 |
| | | Response.Headers.Add("Content-Disposition", $"attachment; filename=\"{System.Web.HttpUtility.UrlEncode(fileName, Encoding.UTF8)}\""); |
| | | Response.Headers.Add("Content-Length", fileInfo.Length.ToString()); |
| | | Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); |
| | | Response.Headers.Add("Pragma", "no-cache"); |
| | | Response.Headers.Add("Expires", "0"); |
| | | |
| | | // 流式返回,不加载整个文件到内存 |
| | | return File(CreateFileStream(fileInfo.FullName), 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<FileInfo> FindLogFileAsync(string fileName) |
| | | { |
| | | var tasks = LogFolders.Select(folderName => Task.Run(() => |
| | | { |
| | | var folderPath = Path.Combine(AppContext.BaseDirectory, folderName); |
| | | var folderInfo = new DirectoryInfo(folderPath); |
| | | |
| | | if (!folderInfo.Exists) |
| | | { |
| | | return null; |
| | | } |
| | | |
| | | return FindFileInFolderAsync(folderInfo, fileName); |
| | | })); |
| | | |
| | | var results = await Task.WhenAll(tasks); |
| | | return results.FirstOrDefault(result => result != null); |
| | | } |
| | | private async Task<FileInfo> FindFileInFolderAsync(DirectoryInfo folder, string fileName) |
| | | { |
| | | try |
| | | { |
| | | // 先查找当前目录下的文件 |
| | | var files = folder.GetFiles(); |
| | | var file = files.FirstOrDefault(x => x.Name.Equals(fileName, StringComparison.OrdinalIgnoreCase)); |
| | | |
| | | if (file != null) |
| | | { |
| | | return await Task.FromResult(file); |
| | | } |
| | | |
| | | // 递归查找子目录(排除Info目录) |
| | | foreach (var subDir in folder.GetDirectories()) |
| | | { |
| | | // 跳过Info目录 |
| | | if (subDir.Name.Equals("Info", StringComparison.OrdinalIgnoreCase)) |
| | | { |
| | | continue; |
| | | } |
| | | |
| | | // 跳过无法访问的目录 |
| | | try |
| | | { |
| | | file = await FindFileInFolderAsync(subDir, fileName); |
| | | if (file != null) |
| | | { |
| | | return file; |
| | | } |
| | | } |
| | | catch (UnauthorizedAccessException) |
| | | { |
| | | // 跳过无权限访问的目录 |
| | | continue; |
| | | } |
| | | } |
| | | } |
| | | catch (UnauthorizedAccessException) |
| | | { |
| | | // 忽略无权访问的目录 |
| | | } |
| | | catch (DirectoryNotFoundException) |
| | | { |
| | | // 忽略不存在的目录 |
| | | } |
| | | |
| | | return await Task.FromResult<FileInfo>(null); |
| | | } |
| | | |
| | | private FileStream CreateFileStream(string filePath) |
| | | { |
| | | return new FileStream( |
| | | filePath, |
| | | FileMode.Open, |
| | | FileAccess.Read, |
| | | FileShare.ReadWrite, // 允许其他进程同时读取/写入 |
| | | 81920, // 缓冲区大小 |
| | | useAsync: true); |
| | | } |
| | | |
| | | /// <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" |
| | | }; |
| | | } |
| | | } |
| | | |
| | | } |
| | | } |