SystemToolsController.cs 6.56 KB
using System.Globalization;
using System.IO.Compression;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Mvc;
using Rcs.Application.Common;

namespace Rcs.Api.Controllers;

[ApiController]
[Route("api/system-tools")]
public class SystemToolsController : ControllerBase
{
    private static readonly Regex LogFileNameRegex = new(
        "^log-(?<date>\\d{8})(?:_\\d+)?\\.txt$",
        RegexOptions.Compiled | RegexOptions.IgnoreCase);

    private readonly ILogger<SystemToolsController> _logger;
    private readonly IWebHostEnvironment _environment;

    public SystemToolsController(
        ILogger<SystemToolsController> logger,
        IWebHostEnvironment environment)
    {
        _logger = logger;
        _environment = environment;
    }

    [HttpGet("log-files")]
    public IActionResult GetLogFiles([FromQuery] LogFileQuery query)
    {
        if (!query.StartTime.HasValue || !query.EndTime.HasValue)
        {
            return BadRequest(ApiResponse.Failed("startTime and endTime are required", StatusCodes.Status400BadRequest));
        }

        if (query.StartTime.Value > query.EndTime.Value)
        {
            return BadRequest(ApiResponse.Failed("startTime cannot be greater than endTime", StatusCodes.Status400BadRequest));
        }

        var logsPath = Path.Combine(_environment.ContentRootPath, "logs");
        if (!Directory.Exists(logsPath))
        {
            _logger.LogWarning("[SystemTools] Logs directory does not exist: {LogsPath}", logsPath);
            return Ok(ApiResponse.Successful(Array.Empty<LogFileListItemDto>()));
        }

        var startDate = DateOnly.FromDateTime(query.StartTime.Value);
        var endDate = DateOnly.FromDateTime(query.EndTime.Value);

        var files = Directory
            .EnumerateFiles(logsPath, "log-*.txt", SearchOption.TopDirectoryOnly)
            .Select(path => new FileInfo(path))
            .Select(file => ToLogFileListItem(file, startDate, endDate))
            .Where(item => item != null)
            .Select(item => item!)
            .OrderByDescending(item => item.FileDate)
            .ThenByDescending(item => item.FileName)
            .ToList();

        return Ok(ApiResponse.Successful(files));
    }

    [HttpPost("log-files/download")]
    public IActionResult DownloadLogFiles([FromBody] DownloadLogFilesRequest? request)
    {
        if (request?.FileNames == null || request.FileNames.Count == 0)
        {
            return BadRequest(ApiResponse.Failed("fileNames cannot be empty", StatusCodes.Status400BadRequest));
        }

        var logsPath = Path.Combine(_environment.ContentRootPath, "logs");
        if (!Directory.Exists(logsPath))
        {
            return BadRequest(ApiResponse.Failed("logs directory does not exist", StatusCodes.Status400BadRequest));
        }

        var validFileNames = new List<string>();
        foreach (var rawName in request.FileNames)
        {
            if (string.IsNullOrWhiteSpace(rawName))
            {
                return BadRequest(ApiResponse.Failed("fileNames contains empty value", StatusCodes.Status400BadRequest));
            }

            var fileName = rawName.Trim();
            var baseName = Path.GetFileName(fileName);
            if (!string.Equals(fileName, baseName, StringComparison.Ordinal))
            {
                return BadRequest(ApiResponse.Failed($"invalid file name: {rawName}", StatusCodes.Status400BadRequest));
            }

            if (!LogFileNameRegex.IsMatch(baseName))
            {
                return BadRequest(ApiResponse.Failed($"invalid file name: {rawName}", StatusCodes.Status400BadRequest));
            }

            if (!validFileNames.Contains(baseName, StringComparer.OrdinalIgnoreCase))
            {
                validFileNames.Add(baseName);
            }
        }

        var missingFiles = validFileNames
            .Where(fileName => !System.IO.File.Exists(Path.Combine(logsPath, fileName)))
            .ToList();

        if (missingFiles.Count > 0)
        {
            var joinedNames = string.Join(", ", missingFiles);
            return BadRequest(ApiResponse.Failed($"log files not found: {joinedNames}", StatusCodes.Status400BadRequest));
        }

        var zipFileName = $"logs-{DateTime.Now:yyyyMMddHHmmss}.zip";
        using var zipBuffer = new MemoryStream();
        using (var zipArchive = new ZipArchive(zipBuffer, ZipArchiveMode.Create, leaveOpen: true))
        {
            foreach (var fileName in validFileNames)
            {
                var fullPath = Path.Combine(logsPath, fileName);
                var entry = zipArchive.CreateEntry(fileName, CompressionLevel.Fastest);
                using var entryStream = entry.Open();
                using var sourceStream = System.IO.File.OpenRead(fullPath);
                sourceStream.CopyTo(entryStream);
            }
        }

        _logger.LogInformation(
            "[SystemTools] Download log files count={Count}, fileName={ZipFileName}",
            validFileNames.Count,
            zipFileName);

        return File(zipBuffer.ToArray(), "application/zip", zipFileName);
    }

    private static LogFileListItemDto? ToLogFileListItem(FileInfo file, DateOnly startDate, DateOnly endDate)
    {
        if (!TryParseLogFileDate(file.Name, out var fileDate))
        {
            return null;
        }

        if (fileDate < startDate || fileDate > endDate)
        {
            return null;
        }

        return new LogFileListItemDto
        {
            FileName = file.Name,
            FileDate = fileDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
            SizeBytes = file.Length,
            LastWriteTime = file.LastWriteTime
        };
    }

    private static bool TryParseLogFileDate(string fileName, out DateOnly date)
    {
        date = default;
        var match = LogFileNameRegex.Match(fileName);
        if (!match.Success)
        {
            return false;
        }

        var dateText = match.Groups["date"].Value;
        return DateOnly.TryParseExact(
            dateText,
            "yyyyMMdd",
            CultureInfo.InvariantCulture,
            DateTimeStyles.None,
            out date);
    }
}

public sealed class LogFileQuery
{
    public DateTime? StartTime { get; set; }

    public DateTime? EndTime { get; set; }
}

public sealed class DownloadLogFilesRequest
{
    public List<string> FileNames { get; set; } = new();
}

public sealed class LogFileListItemDto
{
    public string FileName { get; set; } = string.Empty;

    public string FileDate { get; set; } = string.Empty;

    public long SizeBytes { get; set; }

    public DateTime LastWriteTime { get; set; }
}