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>> 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 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 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 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 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(); } } }