Add complete XML doc and Swagger annotations to all controller endpoints
Every public action now has <summary>, <param>, and <returns> XML docs plus matching SwaggerOperation/SwaggerResponse attributes with typed response descriptions. Class-level summaries added to CvController, JobSearchController, and RagController. Explanatory inline comments removed from FileDownloadController per project conventions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,7 +66,6 @@ namespace Api.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use default file name from settings if not provided
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
fileName = _fileStorageSettings.DefaultFileName;
|
||||
@@ -80,43 +79,30 @@ namespace Api.Controllers
|
||||
_logger.LogInformation("Using default file name from settings: {FileName}", fileName);
|
||||
}
|
||||
|
||||
// Get the file storage path (relative to solution folder)
|
||||
var fileStoragePath = _fileStorageSettings.Path;
|
||||
|
||||
// If path is not absolute, make it relative to the solution root
|
||||
if (!Path.IsPathRooted(fileStoragePath))
|
||||
{
|
||||
var solutionRoot = Directory.GetCurrentDirectory();
|
||||
// Go up from api folder to solution root if needed
|
||||
if (solutionRoot.EndsWith("api", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
solutionRoot = Directory.GetParent(solutionRoot)?.FullName ?? solutionRoot;
|
||||
}
|
||||
fileStoragePath = Path.Combine(solutionRoot, fileStoragePath);
|
||||
}
|
||||
|
||||
// Sanitize fileName to prevent directory traversal attacks
|
||||
var sanitizedFileName = Path.GetFileName(fileName);
|
||||
var filePath = Path.Combine(fileStoragePath, sanitizedFileName);
|
||||
|
||||
// Verify file exists
|
||||
if (!System.IO.File.Exists(filePath))
|
||||
{
|
||||
_logger.LogWarning("File not found: {FilePath}", filePath);
|
||||
return NotFound(new ErrorResponse { Error = "File not found", Code = "file_not_found" });
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
var fileLength = fileInfo.Length;
|
||||
var fileLength = new FileInfo(filePath).Length;
|
||||
|
||||
// Determine content type
|
||||
if (!_contentTypeProvider.TryGetContentType(filePath, out var contentType))
|
||||
{
|
||||
contentType = "application/octet-stream";
|
||||
}
|
||||
|
||||
// Send email notification asynchronously (fire and forget with error handling)
|
||||
// This is done before streaming to ensure notification is sent for both full and range downloads
|
||||
var userIp = HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
@@ -130,19 +116,13 @@ namespace Api.Controllers
|
||||
}
|
||||
});
|
||||
|
||||
// Check if this is a range request
|
||||
var rangeHeader = Request.Headers[HeaderNames.Range].ToString();
|
||||
|
||||
if (!string.IsNullOrEmpty(rangeHeader))
|
||||
{
|
||||
return await HandleRangeRequest(filePath, fileLength, contentType, rangeHeader, sanitizedFileName);
|
||||
}
|
||||
|
||||
// Full file download
|
||||
_logger.LogInformation("Starting full file download: {FileName} ({FileSize} bytes)", sanitizedFileName, fileLength);
|
||||
|
||||
var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, useAsync: true);
|
||||
|
||||
Response.Headers.Append(HeaderNames.AcceptRanges, "bytes");
|
||||
Response.Headers.Append(HeaderNames.ContentLength, fileLength.ToString());
|
||||
|
||||
@@ -167,34 +147,25 @@ namespace Api.Controllers
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse range header (format: "bytes=start-end")
|
||||
var range = rangeHeader.Replace("bytes=", "").Split('-');
|
||||
|
||||
long startByte = 0;
|
||||
long endByte = fileLength - 1;
|
||||
|
||||
if (!string.IsNullOrEmpty(range[0]))
|
||||
{
|
||||
startByte = long.Parse(range[0]);
|
||||
}
|
||||
|
||||
if (range.Length > 1 && !string.IsNullOrEmpty(range[1]))
|
||||
{
|
||||
endByte = long.Parse(range[1]);
|
||||
}
|
||||
|
||||
// Validate range
|
||||
if (startByte > endByte || startByte >= fileLength)
|
||||
{
|
||||
_logger.LogWarning("Invalid range request: {Range} for file size {FileLength}", rangeHeader, fileLength);
|
||||
return StatusCode(StatusCodes.Status416RangeNotSatisfiable);
|
||||
}
|
||||
|
||||
// Adjust end byte if it exceeds file length
|
||||
if (endByte >= fileLength)
|
||||
{
|
||||
endByte = fileLength - 1;
|
||||
}
|
||||
|
||||
var contentLength = endByte - startByte + 1;
|
||||
|
||||
@@ -202,20 +173,16 @@ namespace Api.Controllers
|
||||
"Range request for {FileName}: bytes {Start}-{End}/{Total} ({ContentLength} bytes)",
|
||||
fileName, startByte, endByte, fileLength, contentLength);
|
||||
|
||||
// Open file stream and seek to start position
|
||||
var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, BufferSize, useAsync: true);
|
||||
stream.Seek(startByte, SeekOrigin.Begin);
|
||||
|
||||
// Set response headers for partial content
|
||||
Response.StatusCode = StatusCodes.Status206PartialContent;
|
||||
Response.Headers.Append(HeaderNames.AcceptRanges, "bytes");
|
||||
Response.Headers.Append(HeaderNames.ContentRange, $"bytes {startByte}-{endByte}/{fileLength}");
|
||||
Response.Headers.Append(HeaderNames.ContentLength, contentLength.ToString());
|
||||
Response.ContentType = contentType;
|
||||
|
||||
// Stream the requested range
|
||||
await StreamRangeAsync(stream, Response.Body, contentLength);
|
||||
|
||||
await stream.DisposeAsync();
|
||||
|
||||
return new EmptyResult();
|
||||
@@ -241,9 +208,7 @@ namespace Api.Controllers
|
||||
var bytesRead = await source.ReadAsync(buffer.AsMemory(0, bytesToReadThisIteration));
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
break; // End of stream
|
||||
}
|
||||
break;
|
||||
|
||||
await destination.WriteAsync(buffer.AsMemory(0, bytesRead));
|
||||
totalBytesRead += bytesRead;
|
||||
|
||||
Reference in New Issue
Block a user