把上传文件保存在数据库中

This commit is contained in:
yzp 2025-06-20 11:44:01 +08:00
parent 055d0d5629
commit 27baf40fe2
7 changed files with 168 additions and 10 deletions

View File

@ -1,11 +1,12 @@
{
{
"$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
"Upload": {
"Path": "upload/{yyyy}/{MM}/{dd}", //
"MaxSize": 51200, // KB1024*50
"ContentType": [ "image/jpg", "image/png", "image/jpeg", "image/gif", "image/bmp", "text/plain", "text/xml", "application/pdf", "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "video/mp4", "application/wps-office.docx", "application/wps-office.xlsx", "application/wps-office.pptx", "application/vnd.android.package-archive", "application/octet-stream" ],
"EnableMd5": false // MDF5-
"EnableMd5": false, // MDF5-
"EnableSaveFileToDb": false //便
},
"OSSProvider": {
"Enabled": false,

View File

@ -0,0 +1,27 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
namespace Admin.NET.Core;
/// <summary>
/// 系统文件内容表
/// </summary>
[SugarTable(null, "系统文件内容表")]
[SysTable]
public partial class SysFileContent : EntityBase
{
/// <summary>
/// SysFile里的Id
/// </summary>
[SugarColumn(ColumnDescription = "SysFile里的Id")]
public long SysFileId { get; set; }
/// <summary>
/// 文件内容
/// </summary>
[SugarColumn(ColumnDescription = "文件内容", ColumnDataType = "blob,bytea,binary", IsNullable = false)]
public byte[] Content { get; set; }
}

View File

@ -32,7 +32,13 @@ public sealed class UploadOptions : IConfigurableOptions
/// 启用文件MD5验证
/// </summary>
/// <remarks>防止重复上传</remarks>
public bool EnableMd5 { get; set; }
public bool EnableMd5 { get; set; }
/// <summary>
/// 把文件保存到数据库的表里
/// </summary>
public bool EnableSaveFileToDb { get; set; }
}
/// <summary>

View File

@ -0,0 +1,93 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
using Furion.DependencyInjection;
namespace Admin.NET.Core.Service;
public class DbFileProvider : ICustomFileProvider, ITransient
{
private readonly SqlSugarRepository<SysFileContent> _sysFileContentRep;
public DbFileProvider(SqlSugarRepository<SysFileContent> sysFileContentRep)
{
_sysFileContentRep = sysFileContentRep;
}
public async Task DeleteFileAsync(SysFile sysFile)
{
// 从数据库中删除文件内容
await _sysFileContentRep.DeleteAsync(u => u.SysFileId == sysFile.Id);
}
public async Task<string> DownloadFileBase64Async(SysFile sysFile)
{// 考虑可能会曾经配置成保存到本地文件
if (string.IsNullOrEmpty(sysFile.Provider) || sysFile.Provider == "Local")
{
var provider = App.GetService<DefaultFileProvider>();
return await provider.DownloadFileBase64Async(sysFile);
}
else
{
// 从数据库获取文件内容
var fileContent = await _sysFileContentRep.CopyNew().GetFirstAsync(u => u.SysFileId == sysFile.Id);
if (fileContent == null || fileContent.Content == null)
{
Log.Error($"DbFileProvider.DownloadFileBase64:文件[{sysFile.Id},{sysFile.Url}]内容不存在");
throw Oops.Oh($"文件[{sysFile.FilePath}]内容不存在");
}
return Convert.ToBase64String(fileContent.Content);
}
}
public async Task<FileStreamResult> GetFileStreamResultAsync(SysFile sysFile, string fileName)
{
// 考虑可能会曾经配置成保存到本地文件
if (string.IsNullOrEmpty(sysFile.Provider) || sysFile.Provider == "Local")
{
var provider = App.GetService<DefaultFileProvider>();
return await provider.GetFileStreamResultAsync(sysFile, fileName);
}
else
{
// 从数据库获取文件内容
var fileContent = await _sysFileContentRep.GetFirstAsync(u => u.SysFileId == sysFile.Id);
if (fileContent == null || fileContent.Content == null)
{
Log.Error($"DbFileProvider.GetFileStreamResultAsync:文件[{sysFile.Id},{sysFile.Url}]内容不存在");
throw Oops.Oh($"文件[{sysFile.FilePath}]内容不存在");
}
// 创建内存流
var memoryStream = new MemoryStream(fileContent.Content);
return new FileStreamResult(memoryStream, "application/octet-stream") { FileDownloadName = fileName + sysFile.Suffix };
}
}
public async Task<SysFile> UploadFileAsync(IFormFile file, SysFile newFile, string path, string finalName)
{
newFile.Provider = "Database"; // 数据库存储 Provider 显示为Database
// 读取文件内容到字节数组
byte[] fileContent;
using (var memoryStream = new MemoryStream())
{
await file.CopyToAsync(memoryStream);
fileContent = memoryStream.ToArray();
}
// 保存文件内容到数据库
var sysFileContent = new SysFileContent
{
SysFileId = newFile.Id,
Content = fileContent
};
await _sysFileContentRep.InsertAsync(sysFileContent);
// 设置文件URL
newFile.Url = $"upload/downloadfile?fileMd5={newFile.FileMd5}&id={newFile.Id}&fileName=tmp{newFile.Suffix}";
return newFile;
}
}

View File

@ -40,7 +40,7 @@ public class DefaultFileProvider : ICustomFileProvider, ITransient
public async Task<SysFile> UploadFileAsync(IFormFile file, SysFile newFile, string path, string finalName)
{
newFile.Provider = ""; // 本地存储 Provider 显示为空
newFile.Provider = "Local";
var filePath = Path.Combine(App.WebHostEnvironment.WebRootPath, path);
if (!Directory.Exists(filePath))
Directory.CreateDirectory(filePath);

View File

@ -43,6 +43,10 @@ public class SysFileService : IDynamicApiController, ITransient
{
_customFileProvider = _namedServiceProvider.GetService<ITransient>(nameof(SSHFileProvider));
}
else if (App.Configuration["Upload:EnableSaveFileToDb"].ToBoolean())
{
_customFileProvider = _namedServiceProvider.GetService<ITransient>(nameof(DbFileProvider));
}
else
{
_customFileProvider = _namedServiceProvider.GetService<ITransient>(nameof(DefaultFileProvider));
@ -126,6 +130,31 @@ public class SysFileService : IDynamicApiController, ITransient
return await GetFileStreamResult(file, fileName);
}
/// <summary>
/// 在upload路径下载文件 🔖
/// </summary>
/// <param name="id"></param>
/// <param name="fileMd5"></param>
/// <param name="fileName"></param>
/// <returns></returns>
/// <remarks>
/// 这个接口定义在 /upload/downloadfile与DbFileProvider返回的路径要对应得上
/// 之所以定义在这里有利于使用反向代理指入我们的upload文件夹时不用修改文件的下载路径。
/// 比如我们的前端就把他自己的upload转发到了我们后端的upload路径所以这种情况下我们用db来保存文件时就不用修改下载文件的方式
/// </remarks>
[Route("/upload/downloadfile")]
[AllowAnonymous]
[HttpGet]
[DisplayName("在upload路径")]
public async Task<IActionResult> DownloadFile2([FromQuery]long id, [FromQuery] string fileMd5, [FromQuery] string fileName)
{
var file = await GetFile(id);
if (file.FileMd5 != null && file.FileMd5 != fileMd5)
throw Oops.Bah("文件校验信息不符");
fileName = HttpUtility.UrlEncode(fileName, Encoding.GetEncoding("UTF-8"));
return await GetFileStreamResult(file, fileName);
}
/// <summary>
/// 文件预览 🔖
/// </summary>
@ -248,12 +277,13 @@ public class SysFileService : IDynamicApiController, ITransient
// 判断是否重复上传的文件
var sizeKb = input.File.Length / 1024; // 大小KB
var fileMd5 = string.Empty;
// 不管要不要验证md5也把Md6计算出来方便统计重复的文件。
await using (var fileStream = input.File.OpenReadStream())
{
fileMd5 = OssUtils.ComputeContentMd5(fileStream, fileStream.Length);
}
if (_uploadOptions.EnableMd5)
{
await using (var fileStream = input.File.OpenReadStream())
{
fileMd5 = OssUtils.ComputeContentMd5(fileStream, fileStream.Length);
}
// Mysql8 中如果使用了 utf8mb4_general_ci 之外的编码会出错,尽量避免在条件里使用.ToString()
// 因为 Squsugar 并不是把变量转换为字符串来构造SQL语句而是构造了CAST(123 AS CHAR)这样的语句这样这个返回值是utf8mb4_general_ci所以容易出错。
var sysFile = await _sysFileRep.GetFirstAsync(u => u.FileMd5 == fileMd5 && u.SizeKb == sizeKb);

View File

@ -1,4 +1,4 @@
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
//
// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
//
@ -576,7 +576,8 @@ public class SysTenantService : IDynamicApiController, ITransient
if (tenantId < 1) tenantId = long.Parse(App.User?.FindFirst(ClaimConst.TenantId)?.Value ?? "0");
if (tenantId < 1) tenantId = SqlSugarConst.DefaultTenantId;
var tenant = await _sysTenantRep.GetFirstAsync(u => u.Id == tenantId);
if (tenant == null) return "";
if (tenant == null)
throw Oops.Bah($"租户信息不存在:{tenantId}");
// 若租户系统标题为空,则获取默认租户系统信息(兼容已有未配置的租户)
if (string.IsNullOrWhiteSpace(tenant.Title))