First Commit
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
|
||||
namespace JobTrackerApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/attachments")]
|
||||
public class AttachmentsController : ControllerBase
|
||||
{
|
||||
private readonly AppPaths _paths;
|
||||
private readonly JobTrackerContext _db;
|
||||
|
||||
public AttachmentsController(AppPaths paths, JobTrackerContext db)
|
||||
{
|
||||
_paths = paths;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public sealed record AttachmentDto(int Id, string FileName, DateTime UploadDate, string FileType, long FileSize);
|
||||
|
||||
[HttpGet("{jobId:int}")]
|
||||
public async Task<ActionResult<List<AttachmentDto>>> ListForJob([FromRoute] int jobId, CancellationToken cancellationToken)
|
||||
{
|
||||
var jobOk = await _db.JobApplications.AnyAsync(j => j.Id == jobId, cancellationToken);
|
||||
if (!jobOk) return NotFound();
|
||||
|
||||
var items = await _db.Attachments
|
||||
.AsNoTracking()
|
||||
.Where(a => a.JobApplicationId == jobId)
|
||||
.OrderByDescending(a => a.UploadDate)
|
||||
.Select(a => new AttachmentDto(a.Id, a.FileName, a.UploadDate, a.FileType, a.FileSize))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
[HttpGet("download/{id:int}")]
|
||||
public async Task<IActionResult> Download([FromRoute] int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var att = await _db.Attachments.AsNoTracking().FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
|
||||
if (att is null) return NotFound();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(att.FilePath) || !System.IO.File.Exists(att.FilePath))
|
||||
return NotFound();
|
||||
|
||||
var contentType = string.IsNullOrWhiteSpace(att.FileType) ? "application/octet-stream" : att.FileType;
|
||||
var fileName = Path.GetFileName(att.FileName);
|
||||
return PhysicalFile(att.FilePath, contentType, fileName);
|
||||
}
|
||||
|
||||
public sealed record RenameAttachmentRequest(string FileName);
|
||||
|
||||
[HttpPatch("{id:int}")]
|
||||
public async Task<IActionResult> Rename([FromRoute] int id, [FromBody] RenameAttachmentRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var att = await _db.Attachments.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
|
||||
if (att is null) return NotFound();
|
||||
|
||||
var name = Path.GetFileName((request.FileName ?? "").Trim());
|
||||
if (name.Length == 0) return BadRequest("FileName is required.");
|
||||
|
||||
var folder = Path.GetDirectoryName(att.FilePath) ?? _paths.AttachmentsRoot;
|
||||
var newPath = Path.Combine(folder, name);
|
||||
|
||||
if (System.IO.File.Exists(att.FilePath) && !string.Equals(att.FilePath, newPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
System.IO.File.Move(att.FilePath, newPath, overwrite: true);
|
||||
}
|
||||
|
||||
att.FileName = name;
|
||||
att.FilePath = newPath;
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
public async Task<IActionResult> Delete([FromRoute] int id, CancellationToken cancellationToken)
|
||||
{
|
||||
var att = await _db.Attachments.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
|
||||
if (att is null) return NotFound();
|
||||
|
||||
var path = att.FilePath;
|
||||
_db.Attachments.Remove(att);
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(path) && System.IO.File.Exists(path))
|
||||
System.IO.File.Delete(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best effort
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Upload([FromForm] IFormFileCollection files, [FromForm] int jobId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (jobId <= 0) return BadRequest("Valid jobId is required.");
|
||||
if (files is null || files.Count == 0) return BadRequest("At least one file is required.");
|
||||
|
||||
var jobExists = await _db.JobApplications.AnyAsync(j => j.Id == jobId, cancellationToken);
|
||||
if (!jobExists) return BadRequest("jobId does not exist.");
|
||||
|
||||
var folder = Path.Combine(_paths.AttachmentsRoot, jobId.ToString());
|
||||
Directory.CreateDirectory(folder);
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (file.Length == 0) continue;
|
||||
|
||||
var safeName = Path.GetFileName(file.FileName);
|
||||
var path = Path.Combine(folder, safeName);
|
||||
await using var stream = new FileStream(path, FileMode.Create);
|
||||
await file.CopyToAsync(stream, cancellationToken);
|
||||
|
||||
_db.Attachments.Add(new Attachment
|
||||
{
|
||||
JobApplicationId = jobId,
|
||||
FileName = safeName,
|
||||
FilePath = path,
|
||||
UploadDate = DateTime.Now,
|
||||
FileType = string.IsNullOrWhiteSpace(file.ContentType) ? "application/octet-stream" : file.ContentType,
|
||||
FileSize = file.Length
|
||||
});
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user