647556386
昨天 b680585c3a6d43f0c72a83a115ea537ce8c91a07
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/System/Sys_LogController.cs
@@ -16,9 +16,16 @@
    [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" };
        public Sys_LogController(ISys_LogService service) : base(service)
        {
        }
        [HttpPost, Route("GetLogName"), AllowAnonymous]
        public WebResponseContent GetLogName()
        {
@@ -81,21 +88,44 @@
                if (files.Count > 0)
                {
                    FileInfo file = files.Where(x => x.Name == fileName).FirstOrDefault();
                    using StreamReader stream = new StreamReader(file.FullName);
                    StringBuilder text = new StringBuilder();
                    List<string> lines = new List<string>();
                    int i = 0;
                    while (stream.Peek() >= 0)
                    if (file == null)
                    {
                        var line = stream.ReadLine();
                        lines.Add(line);
                        return WebResponseContent.Instance.Error($"未找到日志文件: {fileName}");
                    }
                    content = WebResponseContent.Instance.OK(data: lines);
                    // ä½¿ç”¨å…±äº«è¯»å–模式
                    using (FileStream stream = new FileStream(
                        file.FullName,
                        FileMode.Open,
                        FileAccess.Read,
                        FileShare.ReadWrite))
                    using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
                    {
                        StringBuilder text = new StringBuilder();
                        List<string> lines = new List<string>();
                        while (!reader.EndOfStream)
                        {
                            var line = reader.ReadLine();
                            lines.Add(line);
                        }
                        content = WebResponseContent.Instance.OK(data: lines);
                    }
                }
                else
                {
                    content = WebResponseContent.Instance.Error($"未找到日志文件,【{fileName}】");
                }
            }
            catch (IOException ex)
            {
                if (IsFileLockedException(ex))
                {
                    content = WebResponseContent.Instance.Error($"日志文件正在被系统写入,请稍后再试");
                }
                else
                {
                    content = WebResponseContent.Instance.Error($"打开日志文件错误,{ex.Message}");
                }
            }
            catch (Exception ex)
@@ -104,8 +134,9 @@
            }
            return content;
        }
        [HttpPost, HttpGet, Route("DownLoadLog"), AllowAnonymous]
        public virtual ActionResult DownLoadLog(string fileName)
        public virtual async Task<ActionResult> DownLoadLog(string fileName)
        {
            try
            {
@@ -113,6 +144,12 @@
                if (string.IsNullOrWhiteSpace(fileName))
                {
                    return BadRequest("文件名不能为空");
                }
                // å®‰å…¨æ€§æ£€æŸ¥ï¼šé˜²æ­¢è·¯å¾„遍历攻击
                if (fileName.Contains("..") || Path.IsPathRooted(fileName))
                {
                    return BadRequest("无效的文件名");
                }
                //string logDirectory = Path.Combine(AppContext.BaseDirectory, "logs");
@@ -138,22 +175,26 @@
                FileInfo fileInfo = new FileInfo(filePath);
                if (fileInfo.Length > 50 * 1024 * 1024) // 50MB限制
                if (fileInfo.Length > MAX_FILE_SIZE_MB * 1024 * 1024)
                {
                    return BadRequest("文件过大,无法下载");
                    return BadRequest($"文件过大,超过{MAX_FILE_SIZE_MB}MB限制");
                }
                byte[] fileBytes;
                using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read))
                // æ–¹æ¡ˆ1:使用重试机制 + å…±äº«è¯»å–(推荐)
                byte[] fileBytes = await ReadFileWithRetryAsync(filePath);
                if (fileBytes == null)
                {
                    using (MemoryStream memoryStream = new MemoryStream())
                    {
                        fileStream.CopyTo(memoryStream);
                        fileBytes = memoryStream.ToArray();
                    }
                    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);
            }
@@ -167,19 +208,79 @@
            }
            catch (IOException ex)
            {
                return StatusCode(500, "文件读取失败,可能正在被其他进程使用");
                if (IsFileLockedException(ex))
                {
                    return StatusCode(500, "文件被锁定,可能正在被系统写入,请稍后再试");
                }
                return StatusCode(500, $"文件读取失败: {ex.Message}");
            }
            catch (Exception ex)
            {
                // è®°å½•异常日志(这里简化为返回,实际项目中应该记录到日志系统)
                return StatusCode(500, $"服务器内部错误: {ex.Message}");
            }
        }
        private bool IsAllowedFileType(string extension)
        /// <summary>
        /// å¸¦é‡è¯•机制的文件读取方法
        /// </summary>
        private async Task<byte[]> ReadFileWithRetryAsync(string filePath)
        {
            var allowedTypes = new[] { ".txt", ".log", ".csv", ".json", ".xml" };
            return allowedTypes.Contains(extension);
            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
@@ -192,5 +293,108 @@
                _ => "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}");
            }
        }
    }
}
}