using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Mvc;
|
using System.Text;
|
using WIDESEA_Core;
|
using WIDESEA_Core.BaseController;
|
using WIDESEA_ISystemService;
|
using WIDESEA_Model.Models;
|
|
namespace WIDESEA_WMSServer.Controllers
|
{
|
/// <summary>
|
/// 日志
|
/// </summary>
|
[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" };
|
|
public Sys_LogController(ISys_LogService service) : base(service)
|
{
|
}
|
|
[HttpPost, Route("GetLogName"), AllowAnonymous]
|
public WebResponseContent GetLogName()
|
{
|
WebResponseContent content = new WebResponseContent();
|
try
|
{
|
List<object> data = new List<object>();
|
DirectoryInfo folder = new DirectoryInfo(AppContext.BaseDirectory + "\\logs\\");
|
DirectoryInfo[] firstDirectoryInfos = folder.GetDirectories().OrderByDescending(x => x.CreationTime).ToArray();
|
int k = 2020;
|
for (int i = 0; i < firstDirectoryInfos.Length; i++)
|
{
|
if (firstDirectoryInfos[i].Name != "Info")
|
{
|
FileInfo[] nextFileInfos = firstDirectoryInfos[i].GetFiles();
|
List<object> values = new List<object>();
|
for (int j = 0; j < nextFileInfos.Length; j++)
|
{
|
values.Add(new { label = nextFileInfos[j].Name, id = k, hidden = true, fatherNode = firstDirectoryInfos[i].Name });
|
k++;
|
}
|
data.Add(new { label = firstDirectoryInfos[i].Name, children = values, id = i, hidden = false });
|
}
|
}
|
|
FileInfo[] nextFileInfo = folder.GetFiles();
|
List<object> value = new List<object>();
|
for (int j = 0; j < nextFileInfo.Length; j++)
|
{
|
value.Add(new { label = nextFileInfo[j].Name, id = k, hidden = true, fatherNode = folder.Name });
|
k++;
|
}
|
data.Add(new { label = folder.Name, children = value, id = 1, hidden = false });
|
|
return WebResponseContent.Instance.OK(data: data);
|
}
|
catch (Exception ex)
|
{
|
return WebResponseContent.Instance.Error(ex.Message);
|
}
|
}
|
|
[HttpPost, Route("GetLog"), AllowAnonymous]
|
public WebResponseContent GetLog(string fileName)
|
{
|
WebResponseContent content = new WebResponseContent();
|
try
|
{
|
List<FileInfo> files = new List<FileInfo>();
|
DirectoryInfo folder = new DirectoryInfo(AppContext.BaseDirectory + "\\logs\\");
|
DirectoryInfo[] firstDirectoryInfos = folder.GetDirectories();
|
for (int i = 0; i < firstDirectoryInfos.Length; i++)
|
{
|
FileInfo[] nextFileInfos = firstDirectoryInfos[i].GetFiles();
|
files.AddRange(nextFileInfos);
|
}
|
|
FileInfo[] nextFileInfo = folder.GetFiles();
|
files.AddRange(nextFileInfo);
|
if (files.Count > 0)
|
{
|
FileInfo file = files.Where(x => x.Name == fileName).FirstOrDefault();
|
if (file == null)
|
{
|
return WebResponseContent.Instance.Error($"未找到日志文件: {fileName}");
|
}
|
|
// 使用共享读取模式
|
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)
|
{
|
content = WebResponseContent.Instance.Error($"打开日志文件错误,{ex.Message}");
|
}
|
return content;
|
}
|
|
[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}");
|
}
|
}
|
}
|
}
|