feat: 파일 업로드/조회/삭제 API 구현 (#98) #99
82
SPMS.API/Controllers/FileController.cs
Normal file
82
SPMS.API/Controllers/FileController.cs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
using SPMS.Application.DTOs.File;
|
||||
using SPMS.Application.Interfaces;
|
||||
using SPMS.Domain.Common;
|
||||
using SPMS.Domain.Exceptions;
|
||||
|
||||
namespace SPMS.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("v1/in/file")]
|
||||
[Authorize]
|
||||
[ApiExplorerSettings(GroupName = "file")]
|
||||
public class FileController : ControllerBase
|
||||
{
|
||||
private readonly IFileService _fileService;
|
||||
|
||||
public FileController(IFileService fileService)
|
||||
{
|
||||
_fileService = fileService;
|
||||
}
|
||||
|
||||
[HttpPost("upload")]
|
||||
[SwaggerOperation(Summary = "파일 업로드", Description = "이미지 또는 CSV 파일을 업로드합니다.")]
|
||||
[RequestSizeLimit(52_428_800)] // 50MB
|
||||
public async Task<IActionResult> UploadAsync(IFormFile file, [FromForm] string file_type)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var adminId = GetAdminId();
|
||||
|
||||
using var stream = file.OpenReadStream();
|
||||
var result = await _fileService.UploadAsync(
|
||||
serviceId, adminId, stream, file.FileName, file.Length, file_type);
|
||||
|
||||
return Ok(ApiResponse<FileUploadResponseDto>.Success(result));
|
||||
}
|
||||
|
||||
[HttpPost("info")]
|
||||
[SwaggerOperation(Summary = "파일 조회", Description = "파일 메타데이터를 조회합니다.")]
|
||||
public async Task<IActionResult> GetInfoAsync([FromBody] FileInfoRequestDto request)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var result = await _fileService.GetInfoAsync(serviceId, request.FileId);
|
||||
return Ok(ApiResponse<FileInfoResponseDto>.Success(result));
|
||||
}
|
||||
|
||||
[HttpPost("list")]
|
||||
[SwaggerOperation(Summary = "파일 목록 조회", Description = "서비스의 파일 목록을 페이징 조회합니다.")]
|
||||
public async Task<IActionResult> GetListAsync([FromBody] FileListRequestDto request)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
var result = await _fileService.GetListAsync(serviceId, request);
|
||||
return Ok(ApiResponse<FileListResponseDto>.Success(result));
|
||||
}
|
||||
|
||||
[HttpPost("delete")]
|
||||
[SwaggerOperation(Summary = "파일 삭제", Description = "파일을 삭제합니다. (Soft Delete)")]
|
||||
public async Task<IActionResult> DeleteAsync([FromBody] FileDeleteRequestDto request)
|
||||
{
|
||||
var serviceId = GetServiceId();
|
||||
await _fileService.DeleteAsync(serviceId, request.FileId);
|
||||
return Ok(ApiResponse.Success());
|
||||
}
|
||||
|
||||
private long GetServiceId()
|
||||
{
|
||||
if (HttpContext.Items.TryGetValue("ServiceId", out var serviceIdObj) && serviceIdObj is long serviceId)
|
||||
return serviceId;
|
||||
|
||||
throw new SpmsException(ErrorCodes.BadRequest, "서비스 식별 정보가 없습니다.", 400);
|
||||
}
|
||||
|
||||
private long GetAdminId()
|
||||
{
|
||||
var adminIdClaim = User.FindFirst("adminId")?.Value;
|
||||
if (string.IsNullOrEmpty(adminIdClaim) || !long.TryParse(adminIdClaim, out var adminId))
|
||||
throw new SpmsException(ErrorCodes.Unauthorized, "인증 정보가 올바르지 않습니다.", 401);
|
||||
|
||||
return adminId;
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,10 @@
|
|||
"CredentialEncryption": {
|
||||
"Key": ""
|
||||
},
|
||||
"FileStorage": {
|
||||
"UploadPath": "Uploads",
|
||||
"BaseUrl": "/uploads"
|
||||
},
|
||||
"Serilog": {
|
||||
"Using": ["Serilog.Sinks.Console", "Serilog.Sinks.File"],
|
||||
"MinimumLevel": {
|
||||
|
|
|
|||
11
SPMS.Application/DTOs/File/FileDeleteRequestDto.cs
Normal file
11
SPMS.Application/DTOs/File/FileDeleteRequestDto.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.File;
|
||||
|
||||
public class FileDeleteRequestDto
|
||||
{
|
||||
[Required]
|
||||
[JsonPropertyName("file_id")]
|
||||
public long FileId { get; set; }
|
||||
}
|
||||
11
SPMS.Application/DTOs/File/FileInfoRequestDto.cs
Normal file
11
SPMS.Application/DTOs/File/FileInfoRequestDto.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.File;
|
||||
|
||||
public class FileInfoRequestDto
|
||||
{
|
||||
[Required]
|
||||
[JsonPropertyName("file_id")]
|
||||
public long FileId { get; set; }
|
||||
}
|
||||
30
SPMS.Application/DTOs/File/FileInfoResponseDto.cs
Normal file
30
SPMS.Application/DTOs/File/FileInfoResponseDto.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.File;
|
||||
|
||||
public class FileInfoResponseDto
|
||||
{
|
||||
[JsonPropertyName("file_id")]
|
||||
public long FileId { get; set; }
|
||||
|
||||
[JsonPropertyName("service_code")]
|
||||
public string ServiceCode { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("file_name")]
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("file_url")]
|
||||
public string FileUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("file_size")]
|
||||
public long FileSize { get; set; }
|
||||
|
||||
[JsonPropertyName("file_type")]
|
||||
public string FileType { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("uploaded_by")]
|
||||
public string UploadedBy { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
15
SPMS.Application/DTOs/File/FileListRequestDto.cs
Normal file
15
SPMS.Application/DTOs/File/FileListRequestDto.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.File;
|
||||
|
||||
public class FileListRequestDto
|
||||
{
|
||||
[JsonPropertyName("page")]
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
[JsonPropertyName("size")]
|
||||
public int Size { get; set; } = 20;
|
||||
|
||||
[JsonPropertyName("file_type")]
|
||||
public string? FileType { get; set; }
|
||||
}
|
||||
13
SPMS.Application/DTOs/File/FileListResponseDto.cs
Normal file
13
SPMS.Application/DTOs/File/FileListResponseDto.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using SPMS.Application.DTOs.Notice;
|
||||
|
||||
namespace SPMS.Application.DTOs.File;
|
||||
|
||||
public class FileListResponseDto
|
||||
{
|
||||
[JsonPropertyName("items")]
|
||||
public List<FileSummaryDto> Items { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("pagination")]
|
||||
public PaginationDto Pagination { get; set; } = null!;
|
||||
}
|
||||
24
SPMS.Application/DTOs/File/FileSummaryDto.cs
Normal file
24
SPMS.Application/DTOs/File/FileSummaryDto.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.File;
|
||||
|
||||
public class FileSummaryDto
|
||||
{
|
||||
[JsonPropertyName("file_id")]
|
||||
public long FileId { get; set; }
|
||||
|
||||
[JsonPropertyName("file_name")]
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("file_url")]
|
||||
public string FileUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("file_size")]
|
||||
public long FileSize { get; set; }
|
||||
|
||||
[JsonPropertyName("file_type")]
|
||||
public string FileType { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
9
SPMS.Application/DTOs/File/FileUploadRequestDto.cs
Normal file
9
SPMS.Application/DTOs/File/FileUploadRequestDto.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace SPMS.Application.DTOs.File;
|
||||
|
||||
public class FileUploadRequestDto
|
||||
{
|
||||
[Required]
|
||||
public string FileType { get; set; } = string.Empty;
|
||||
}
|
||||
24
SPMS.Application/DTOs/File/FileUploadResponseDto.cs
Normal file
24
SPMS.Application/DTOs/File/FileUploadResponseDto.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace SPMS.Application.DTOs.File;
|
||||
|
||||
public class FileUploadResponseDto
|
||||
{
|
||||
[JsonPropertyName("file_id")]
|
||||
public long FileId { get; set; }
|
||||
|
||||
[JsonPropertyName("file_name")]
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("file_url")]
|
||||
public string FileUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("file_size")]
|
||||
public long FileSize { get; set; }
|
||||
|
||||
[JsonPropertyName("file_type")]
|
||||
public string FileType { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ public static class DependencyInjection
|
|||
services.AddScoped<IFaqService, FaqService>();
|
||||
services.AddScoped<IAppConfigService, AppConfigService>();
|
||||
services.AddScoped<IDeviceService, DeviceService>();
|
||||
services.AddScoped<IFileService, FileService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
|
|
|||
11
SPMS.Application/Interfaces/IFileService.cs
Normal file
11
SPMS.Application/Interfaces/IFileService.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
using SPMS.Application.DTOs.File;
|
||||
|
||||
namespace SPMS.Application.Interfaces;
|
||||
|
||||
public interface IFileService
|
||||
{
|
||||
Task<FileUploadResponseDto> UploadAsync(long serviceId, long adminId, Stream fileStream, string fileName, long fileSize, string fileType);
|
||||
Task<FileInfoResponseDto> GetInfoAsync(long serviceId, long fileId);
|
||||
Task<FileListResponseDto> GetListAsync(long serviceId, FileListRequestDto request);
|
||||
Task DeleteAsync(long serviceId, long fileId);
|
||||
}
|
||||
8
SPMS.Application/Interfaces/IFileStorageService.cs
Normal file
8
SPMS.Application/Interfaces/IFileStorageService.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace SPMS.Application.Interfaces;
|
||||
|
||||
public interface IFileStorageService
|
||||
{
|
||||
Task<string> SaveAsync(long serviceId, string fileName, Stream fileStream);
|
||||
Task DeleteAsync(string filePath);
|
||||
string GetFileUrl(string filePath);
|
||||
}
|
||||
181
SPMS.Application/Services/FileService.cs
Normal file
181
SPMS.Application/Services/FileService.cs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
using SPMS.Application.DTOs.File;
|
||||
using SPMS.Application.DTOs.Notice;
|
||||
using SPMS.Application.Interfaces;
|
||||
using SPMS.Domain.Common;
|
||||
using SPMS.Domain.Entities;
|
||||
using SPMS.Domain.Exceptions;
|
||||
using SPMS.Domain.Interfaces;
|
||||
|
||||
namespace SPMS.Application.Services;
|
||||
|
||||
public class FileService : IFileService
|
||||
{
|
||||
private readonly IFileRepository _fileRepository;
|
||||
private readonly IFileStorageService _fileStorageService;
|
||||
private readonly IServiceRepository _serviceRepository;
|
||||
private readonly IAdminRepository _adminRepository;
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
|
||||
private static readonly HashSet<string> AllowedImageExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{ ".jpg", ".jpeg", ".png", ".gif" };
|
||||
private static readonly HashSet<string> AllowedCsvExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{ ".csv" };
|
||||
|
||||
private const long MaxImageSize = 5 * 1024 * 1024; // 5MB
|
||||
private const long MaxCsvSize = 50 * 1024 * 1024; // 50MB
|
||||
|
||||
public FileService(
|
||||
IFileRepository fileRepository,
|
||||
IFileStorageService fileStorageService,
|
||||
IServiceRepository serviceRepository,
|
||||
IAdminRepository adminRepository,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
_fileRepository = fileRepository;
|
||||
_fileStorageService = fileStorageService;
|
||||
_serviceRepository = serviceRepository;
|
||||
_adminRepository = adminRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public async Task<FileUploadResponseDto> UploadAsync(
|
||||
long serviceId, long adminId, Stream fileStream,
|
||||
string fileName, long fileSize, string fileType)
|
||||
{
|
||||
ValidateFileType(fileType, fileName, fileSize);
|
||||
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
var mimeType = GetMimeType(extension);
|
||||
|
||||
var savedPath = await _fileStorageService.SaveAsync(serviceId, fileName, fileStream);
|
||||
|
||||
var entity = new FileEntity
|
||||
{
|
||||
ServiceId = serviceId,
|
||||
FileName = fileName,
|
||||
FilePath = savedPath,
|
||||
FileSize = fileSize,
|
||||
FileType = fileType.ToLowerInvariant(),
|
||||
MimeType = mimeType,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedBy = adminId
|
||||
};
|
||||
|
||||
await _fileRepository.AddAsync(entity);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
return new FileUploadResponseDto
|
||||
{
|
||||
FileId = entity.Id,
|
||||
FileName = entity.FileName,
|
||||
FileUrl = _fileStorageService.GetFileUrl(entity.FilePath),
|
||||
FileSize = entity.FileSize,
|
||||
FileType = entity.FileType,
|
||||
CreatedAt = entity.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<FileInfoResponseDto> GetInfoAsync(long serviceId, long fileId)
|
||||
{
|
||||
var file = await _fileRepository.GetByIdAndServiceAsync(fileId, serviceId);
|
||||
if (file == null || file.IsDeleted)
|
||||
throw new SpmsException(ErrorCodes.FileNotFound, "존재하지 않는 파일입니다.", 404);
|
||||
|
||||
var service = await _serviceRepository.GetByIdAsync(file.ServiceId);
|
||||
var admin = await _adminRepository.GetByIdAsync(file.CreatedBy);
|
||||
|
||||
return new FileInfoResponseDto
|
||||
{
|
||||
FileId = file.Id,
|
||||
ServiceCode = service?.ServiceCode ?? string.Empty,
|
||||
FileName = file.FileName,
|
||||
FileUrl = _fileStorageService.GetFileUrl(file.FilePath),
|
||||
FileSize = file.FileSize,
|
||||
FileType = file.FileType,
|
||||
UploadedBy = admin?.Email ?? string.Empty,
|
||||
CreatedAt = file.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<FileListResponseDto> GetListAsync(long serviceId, FileListRequestDto request)
|
||||
{
|
||||
var (items, totalCount) = await _fileRepository.GetPagedByServiceAsync(
|
||||
serviceId, request.Page, request.Size, request.FileType);
|
||||
|
||||
var totalPages = (int)Math.Ceiling((double)totalCount / request.Size);
|
||||
|
||||
return new FileListResponseDto
|
||||
{
|
||||
Items = items.Select(f => new FileSummaryDto
|
||||
{
|
||||
FileId = f.Id,
|
||||
FileName = f.FileName,
|
||||
FileUrl = _fileStorageService.GetFileUrl(f.FilePath),
|
||||
FileSize = f.FileSize,
|
||||
FileType = f.FileType,
|
||||
CreatedAt = f.CreatedAt
|
||||
}).ToList(),
|
||||
Pagination = new PaginationDto
|
||||
{
|
||||
Page = request.Page,
|
||||
Size = request.Size,
|
||||
TotalCount = totalCount,
|
||||
TotalPages = totalPages
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(long serviceId, long fileId)
|
||||
{
|
||||
var file = await _fileRepository.GetByIdAndServiceAsync(fileId, serviceId);
|
||||
if (file == null || file.IsDeleted)
|
||||
throw new SpmsException(ErrorCodes.FileNotFound, "존재하지 않는 파일입니다.", 404);
|
||||
|
||||
file.IsDeleted = true;
|
||||
file.DeletedAt = DateTime.UtcNow;
|
||||
_fileRepository.Update(file);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static void ValidateFileType(string fileType, string fileName, long fileSize)
|
||||
{
|
||||
var extension = Path.GetExtension(fileName).ToLowerInvariant();
|
||||
|
||||
switch (fileType.ToLowerInvariant())
|
||||
{
|
||||
case "image":
|
||||
if (!AllowedImageExtensions.Contains(extension))
|
||||
throw new SpmsException(ErrorCodes.FileTypeNotAllowed,
|
||||
"허용되지 않는 이미지 형식입니다. (jpg, png, gif만 가능)", 400);
|
||||
if (fileSize > MaxImageSize)
|
||||
throw new SpmsException(ErrorCodes.FileSizeExceeded,
|
||||
"이미지 파일은 5MB를 초과할 수 없습니다.", 400);
|
||||
break;
|
||||
|
||||
case "csv":
|
||||
if (!AllowedCsvExtensions.Contains(extension))
|
||||
throw new SpmsException(ErrorCodes.FileTypeNotAllowed,
|
||||
"허용되지 않는 파일 형식입니다. (csv만 가능)", 400);
|
||||
if (fileSize > MaxCsvSize)
|
||||
throw new SpmsException(ErrorCodes.FileSizeExceeded,
|
||||
"CSV 파일은 50MB를 초과할 수 없습니다.", 400);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new SpmsException(ErrorCodes.FileTypeNotAllowed,
|
||||
"유효하지 않은 파일 타입입니다. (image, csv만 가능)", 400);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetMimeType(string extension)
|
||||
{
|
||||
return extension switch
|
||||
{
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".png" => "image/png",
|
||||
".gif" => "image/gif",
|
||||
".csv" => "text/csv",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -39,4 +39,9 @@ public static class ErrorCodes
|
|||
// === Push (6) ===
|
||||
public const string PushSendFailed = "161";
|
||||
public const string PushStateChangeNotAllowed = "162";
|
||||
|
||||
// === File (8) ===
|
||||
public const string FileNotFound = "181";
|
||||
public const string FileTypeNotAllowed = "182";
|
||||
public const string FileSizeExceeded = "183";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ public class FileEntity : BaseEntity
|
|||
public string? MimeType { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public long CreatedBy { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
|
||||
// Navigation
|
||||
public Service Service { get; set; } = null!;
|
||||
|
|
|
|||
|
|
@ -6,4 +6,7 @@ public interface IFileRepository : IRepository<FileEntity>
|
|||
{
|
||||
Task<FileEntity?> GetByFileNameAsync(long serviceId, string fileName);
|
||||
Task<bool> FileExistsAsync(long serviceId, string fileName);
|
||||
Task<FileEntity?> GetByIdAndServiceAsync(long id, long serviceId);
|
||||
Task<(IReadOnlyList<FileEntity> Items, int TotalCount)> GetPagedByServiceAsync(
|
||||
long serviceId, int page, int size, string? fileType = null);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,16 @@ public static class DependencyInjection
|
|||
services.AddScoped<IFaqRepository, FaqRepository>();
|
||||
services.AddScoped<IAppConfigRepository, AppConfigRepository>();
|
||||
services.AddScoped<IDeviceRepository, DeviceRepository>();
|
||||
services.AddScoped<IFileRepository, FileRepository>();
|
||||
|
||||
// External Services
|
||||
services.AddScoped<IJwtService, JwtService>();
|
||||
services.AddSingleton<IE2EEService, E2EEService>();
|
||||
services.AddSingleton<ICredentialEncryptionService, CredentialEncryptionService>();
|
||||
|
||||
// File Storage
|
||||
services.AddSingleton<IFileStorageService, LocalFileStorageService>();
|
||||
|
||||
// Token Store & Email Service
|
||||
services.AddMemoryCache();
|
||||
services.AddSingleton<ITokenStore, InMemoryTokenStore>();
|
||||
|
|
|
|||
1099
SPMS.Infrastructure/Migrations/20260210060138_AddFileEntitySoftDelete.Designer.cs
generated
Normal file
1099
SPMS.Infrastructure/Migrations/20260210060138_AddFileEntitySoftDelete.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,40 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SPMS.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddFileEntitySoftDelete : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateTime>(
|
||||
name: "DeletedAt",
|
||||
table: "File",
|
||||
type: "datetime(6)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
table: "File",
|
||||
type: "tinyint(1)",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeletedAt",
|
||||
table: "File");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsDeleted",
|
||||
table: "File");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -359,6 +359,9 @@ namespace SPMS.Infrastructure.Migrations
|
|||
b.Property<long>("CreatedBy")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
|
|
@ -377,6 +380,11 @@ namespace SPMS.Infrastructure.Migrations
|
|||
.HasMaxLength(20)
|
||||
.HasColumnType("varchar(20)");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("tinyint(1)")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("varchar(100)");
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ public class FileEntityConfiguration : IEntityTypeConfiguration<FileEntity>
|
|||
builder.Property(e => e.MimeType).HasMaxLength(100);
|
||||
builder.Property(e => e.CreatedAt).IsRequired();
|
||||
builder.Property(e => e.CreatedBy).IsRequired();
|
||||
builder.Property(e => e.IsDeleted).HasDefaultValue(false);
|
||||
builder.Property(e => e.DeletedAt);
|
||||
|
||||
builder.HasOne(e => e.Service)
|
||||
.WithMany()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using SPMS.Domain.Entities;
|
||||
using SPMS.Domain.Interfaces;
|
||||
|
||||
namespace SPMS.Infrastructure.Persistence.Repositories;
|
||||
|
||||
public class FileRepository : Repository<FileEntity>, IFileRepository
|
||||
{
|
||||
public FileRepository(AppDbContext context) : base(context) { }
|
||||
|
||||
public async Task<FileEntity?> GetByFileNameAsync(long serviceId, string fileName)
|
||||
{
|
||||
return await _dbSet
|
||||
.FirstOrDefaultAsync(f => f.ServiceId == serviceId && f.FileName == fileName && !f.IsDeleted);
|
||||
}
|
||||
|
||||
public async Task<bool> FileExistsAsync(long serviceId, string fileName)
|
||||
{
|
||||
return await _dbSet
|
||||
.AnyAsync(f => f.ServiceId == serviceId && f.FileName == fileName && !f.IsDeleted);
|
||||
}
|
||||
|
||||
public async Task<FileEntity?> GetByIdAndServiceAsync(long id, long serviceId)
|
||||
{
|
||||
return await _dbSet
|
||||
.FirstOrDefaultAsync(f => f.Id == id && f.ServiceId == serviceId);
|
||||
}
|
||||
|
||||
public async Task<(IReadOnlyList<FileEntity> Items, int TotalCount)> GetPagedByServiceAsync(
|
||||
long serviceId, int page, int size, string? fileType = null)
|
||||
{
|
||||
var query = _dbSet.Where(f => f.ServiceId == serviceId && !f.IsDeleted);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(fileType))
|
||||
query = query.Where(f => f.FileType == fileType.ToLowerInvariant());
|
||||
|
||||
var totalCount = await query.CountAsync();
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.Skip((page - 1) * size)
|
||||
.Take(size)
|
||||
.ToListAsync();
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
}
|
||||
43
SPMS.Infrastructure/Services/LocalFileStorageService.cs
Normal file
43
SPMS.Infrastructure/Services/LocalFileStorageService.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using SPMS.Application.Interfaces;
|
||||
|
||||
namespace SPMS.Infrastructure.Services;
|
||||
|
||||
public class LocalFileStorageService : IFileStorageService
|
||||
{
|
||||
private readonly string _uploadPath;
|
||||
private readonly string _baseUrl;
|
||||
|
||||
public LocalFileStorageService(IConfiguration configuration)
|
||||
{
|
||||
_uploadPath = configuration["FileStorage:UploadPath"] ?? "Uploads";
|
||||
_baseUrl = configuration["FileStorage:BaseUrl"] ?? "/uploads";
|
||||
}
|
||||
|
||||
public async Task<string> SaveAsync(long serviceId, string fileName, Stream fileStream)
|
||||
{
|
||||
var directory = Path.Combine(_uploadPath, serviceId.ToString());
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var uniqueName = $"{Guid.NewGuid():N}_{fileName}";
|
||||
var fullPath = Path.Combine(directory, uniqueName);
|
||||
|
||||
using var output = new FileStream(fullPath, FileMode.Create);
|
||||
await fileStream.CopyToAsync(output);
|
||||
|
||||
return $"{serviceId}/{uniqueName}";
|
||||
}
|
||||
|
||||
public Task DeleteAsync(string filePath)
|
||||
{
|
||||
var fullPath = Path.Combine(_uploadPath, filePath);
|
||||
if (System.IO.File.Exists(fullPath))
|
||||
System.IO.File.Delete(fullPath);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetFileUrl(string filePath)
|
||||
{
|
||||
return $"{_baseUrl.TrimEnd('/')}/{filePath}";
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user