// Copyright (c) 2022-Now 少林寺驻北固山办事处大神父王喇嘛 // // SimpleAdmin 基于 Apache License Version 2.0 协议发布,可用于商业项目,但必须遵守以下补充条款: // 1.请不要删除和修改根目录下的LICENSE文件。 // 2.请不要删除和修改SimpleAdmin源码头部的版权声明。 // 3.分发源码时候,请注明软件出处 https://gitee.com/dotnetmoyu/SimpleAdmin // 4.基于本软件的作品,只能使用 SimpleAdmin 作为后台服务,除外情况不可商用且不允许二次分发或开源。 // 5.请不得将本软件应用于危害国家安全、荣誉和利益的行为,不能以任何形式用于非法为目的的行为。 // 6.任何基于本软件而产生的一切法律纠纷和责任,均于我司无关。 using Aliyun.OSS; namespace SimpleAdmin.System; /// /// /// public class FileService : DbRepository, IFileService { private static readonly HttpClient OssPublicHttpClient = new(); private readonly IConfigService _configService; private readonly IAliyunOssClientProvider _ossClientProvider; public FileService( IConfigService configService, IAliyunOssClientProvider ossClientProvider) { _configService = configService; _ossClientProvider = ossClientProvider; } /// public async Task> Page(FilePageInput input) { var query = Context.Queryable().WhereIF(!string.IsNullOrEmpty(input.Engine), it => it.Engine == input.Engine)//根据关键字查询 .WhereIF(!string.IsNullOrEmpty(input.SearchKey), it => it.Name.Contains(input.SearchKey))//根据关键字查询 .OrderByIF(!string.IsNullOrEmpty(input.SortField), input.GetSafeOrderBy("Id", "Name", "Engine", "Bucket", "Suffix", "SizeKb", "SizeInfo", "CreateTime", "UpdateTime", "CreateUser"))//排序 .OrderBy(it => it.Id); var pageInfo = await query.ToPagedListAsync(input.PageNum, input.PageSize);//分页 return pageInfo; } /// public async Task UploadFile(string engine, IFormFile file) { return await StorageFile(engine, file); } /// public async Task Delete(BaseIdListInput input) { var ids = input.Ids;//获取ID await DeleteByIdsAsync(ids.Cast().ToArray());//根据ID删除数据库 } /// public FileStreamResult GetFileStreamResult(string path, string fileName, bool isPathFolder = false) { if (isPathFolder) path = path.CombinePath(fileName); fileName = HttpUtility.UrlEncode(fileName, Encoding.GetEncoding("UTF-8"));//文件名转utf8不然前端下载会乱码 //文件转流 var result = new FileStreamResult(new FileStream(path, FileMode.Open), "application/octet-stream") { FileDownloadName = fileName }; return result; } public async Task GetFileStreamResultFromMinio(string objectName, string fileName) { var minioService = App.GetService(); var stream = await minioService.DownloadFileAsync(objectName); fileName = HttpUtility.UrlEncode(fileName, Encoding.GetEncoding("UTF-8"));//文件名转utf8不然前端下载会乱码 //文件转流 var result = new FileStreamResult(stream, "application/octet-stream") { FileDownloadName = fileName }; return result; } /// public FileStreamResult GetFileStreamResult(byte[] byteArray, string fileName) { fileName = HttpUtility.UrlEncode(fileName, Encoding.GetEncoding("UTF-8"));//文件名转utf8不然前端下载会乱码 //文件转流 var result = new FileStreamResult(new MemoryStream(byteArray), "application/octet-stream") { FileDownloadName = fileName }; return result; } /// public async Task Download(BaseIdInput input) { var devFile = await GetByIdAsync(input.Id); if (devFile != null) { if (devFile.Engine == SysDictConst.FILE_ENGINE_LOCAL) return GetFileStreamResult(devFile.StoragePath, devFile.Name); if (devFile.Engine == SysDictConst.FILE_ENGINE_MINIO) return await GetFileStreamResultFromMinio(devFile.ObjName, devFile.Name); if (devFile.Engine == SysDictConst.FILE_ENGINE_ALIYUN_OSS) return await GetFileStreamResultFromAliyunOss(devFile); return null; } return null; } public async Task StoreGeneratedFile(string engine, string fileName, byte[] content) { await using var stream = new MemoryStream(content); IFormFile file = new FormFile(stream, 0, content.Length, "file", fileName) { Headers = new HeaderDictionary(), ContentType = ResolveGeneratedContentType(fileName) }; return await StorageFile(engine, file); } /// /// 根据归档文件扩展名设置 OSS 元数据,避免 PDF、ZIP 被错误标记为 DOCX。 /// private static string ResolveGeneratedContentType(string fileName) { return Path.GetExtension(fileName).ToLowerInvariant() switch { ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".pdf" => "application/pdf", ".zip" => "application/zip", ".png" => "image/png", ".jpg" or ".jpeg" => "image/jpeg", _ => "application/octet-stream" }; } #region 方法 /// /// 存储文件 /// /// /// private async Task StorageFile(string engine, IFormFile file) { var bucketName = string.Empty;// 存储桶名称 var storageUrl = string.Empty;// 定义存储的url,本地文件返回文件实际路径,其他引擎返回网络地址 var objName = string.Empty;// 定义存储的url,本地文件返回文件实际路径,其他引擎返回网络地址 var objectId = CommonUtils.GetSingleId();//生成id switch (engine) { //存储本地 case SysDictConst.FILE_ENGINE_LOCAL: bucketName = "defaultBucketName";// 存储桶名称 storageUrl = await StorageLocal(objectId, file); break; //存储本地 case SysDictConst.FILE_ENGINE_MINIO: var config = await _configService.GetByConfigKey(CateGoryConst.CONFIG_FILE_MINIO, SysConfigConst.FILE_MINIO_DEFAULT_BUCKET_NAME); if (config != null) { bucketName = config.ConfigValue;// 存储桶名称 (objName, storageUrl) = await StorageMinio(objectId, file); } break; case SysDictConst.FILE_ENGINE_ALIYUN_OSS: (bucketName, objName, storageUrl) = await StorageAliyunOss(objectId, file); break; default: throw Oops.Bah("不支持的文件引擎"); } var fileSizeKb = (long)(file.Length / 1024.0);// 文件大小KB var fileSuffix = Path.GetExtension(file.FileName).ToLower();// 文件后缀 var devFile = new SysFile { Id = objectId, Engine = engine, Bucket = bucketName, Name = file.FileName, Suffix = fileSuffix.TrimStart('.'), ObjName = objName, SizeKb = fileSizeKb, SizeInfo = GetSizeInfo(fileSizeKb), StoragePath = storageUrl }; if (engine != SysDictConst.FILE_ENGINE_LOCAL)//如果不是本地,设置下载地址 { devFile.DownloadPath = storageUrl; } //如果是图片,生成缩略图 if (IsPic(fileSuffix)) { //$"data:image/png;base64," + imgByte; await using var fileStream = file.OpenReadStream();//获取文件流 var image = SKImage.FromEncodedData(fileStream);//获取图片 var bmp = SKBitmap.FromImage(image); var thubnail = bmp.GetPicThumbnail(100, 100);//压缩图片 var thubnailBase64 = ImageUtil.ImgToBase64String(thubnail);//转base64 devFile.Thumbnail = "data:image/png;base64," + thubnailBase64; } await InsertAsync(devFile); return objectId; } /// /// 存储本地文件 /// /// /// private async Task StorageLocal(long fileId, IFormFile file) { string uploadFileFolder; var configKey = string.Empty; //判断是windows还是linux if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { configKey = SysConfigConst.FILE_LOCAL_FOLDER_FOR_UNIX;//Linux } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { configKey = SysConfigConst.FILE_LOCAL_FOLDER_FOR_WINDOWS;//Windows } //获取路径配置 var config = await _configService.GetByConfigKey(CateGoryConst.CONFIG_FILE_LOCAL, configKey); if (config != null) { uploadFileFolder = config.ConfigValue;//赋值路径 var now = DateTime.Now.ToString("d"); var filePath = Path.Combine(uploadFileFolder, now); if (!Directory.Exists(filePath))//如果不存在就创建文件夹 Directory.CreateDirectory(filePath); var fileSuffix = Path.GetExtension(file.FileName).ToLower();// 文件后缀 var fileObjectName = $"{fileId}{fileSuffix}";//存储后的文件名 var fileName = Path.Combine(filePath, fileObjectName);//获取文件全路局 fileName = fileName.Replace("\\", "/");//格式化一系 //存储文件 using (var stream = File.Create(Path.Combine(filePath, fileObjectName))) { await file.CopyToAsync(stream); } return fileName; } throw Oops.Oh("文件存储路径未配置"); } /// /// 存储到minio /// /// /// /// private async Task<(string objName, string downloadUrl)> StorageMinio(long fileId, IFormFile file) { var minioService = App.GetService(); var now = DateTime.Now.ToString("d"); var fileSuffix = Path.GetExtension(file.FileName).ToLower();// 文件后缀 var fileObjectName = $"{now}/{fileId}{fileSuffix}";//存储后的文件名 var downloadUrl = await minioService.PutObjectAsync(fileObjectName, file); return (fileObjectName, downloadUrl); } /// /// 将入职业务文件存储到阿里云 OSS。 /// private async Task<(string bucketName, string objName, string downloadUrl)> StorageAliyunOss( long fileId, IFormFile file) { var setting = GetOssSetting(); var suffix = Path.GetExtension(file.FileName).ToLowerInvariant(); var objectName = $"onboarding/{DateTime.Now:yyyy/MM/dd}/{fileId}{suffix}"; var metadata = new ObjectMetadata(); if (!string.IsNullOrWhiteSpace(file.ContentType)) metadata.ContentType = file.ContentType; await using var stream = file.OpenReadStream(); var client = await _ossClientProvider.GetClientAsync(); client.PutObject(setting.Bucket, objectName, stream, metadata); var downloadUrl = string.IsNullOrWhiteSpace(setting.OSSUrl) ? $"https://{setting.Bucket}.{setting.EndPoint.Trim().TrimEnd('/')}/{objectName}" : $"{setting.OSSUrl.Trim().TrimEnd('/')}/{objectName}"; return (setting.Bucket, objectName, downloadUrl); } /// /// 从阿里云 OSS 读取业务文件并通过统一下载接口返回。 /// private async Task GetFileStreamResultFromAliyunOss(SysFile file) { var setting = GetOssSetting(); try { var client = await _ossClientProvider.GetClientAsync(); var ossObject = client.GetObject(file.Bucket, file.ObjName); await using (ossObject.Content) { return await BuildFileStreamResult(ossObject.Content, file.Name); } } catch { // STS/OSS SDK 暂时不可用时,只通过已公开的对象地址读取文件。 // 后台下载接口仍由系统鉴权保护,且该降级过程不会使用或暴露长期 AK。 var publicUrl = BuildOssPublicUrl(file, setting); using var response = await OssPublicHttpClient.GetAsync( publicUrl, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); await using var content = await response.Content.ReadAsStreamAsync(); return await BuildFileStreamResult(content, file.Name); } } /// /// 将远程文件流复制到独立内存流,避免远程响应释放后影响文件下载。 /// private static async Task BuildFileStreamResult( Stream source, string originalFileName) { var memory = new MemoryStream(); await source.CopyToAsync(memory); memory.Position = 0; var fileName = HttpUtility.UrlEncode( originalFileName, Encoding.GetEncoding("UTF-8")); return new FileStreamResult(memory, "application/octet-stream") { FileDownloadName = fileName }; } /// /// 根据受信任的 OSS 配置生成公开对象地址,不使用数据库中的任意外部地址。 /// private static string BuildOssPublicUrl(SysFile file, OssSettingOptions setting) { var baseUrl = string.IsNullOrWhiteSpace(setting.OSSUrl) ? $"https://{file.Bucket}.{setting.EndPoint.Trim().TrimEnd('/')}" : setting.OSSUrl.Trim().TrimEnd('/'); var objectPath = string.Join( "/", file.ObjName.Split('/', StringSplitOptions.RemoveEmptyEntries) .Select(Uri.EscapeDataString)); return $"{baseUrl}/{objectPath}"; } /// /// 读取并校验阿里云 OSS 配置。 /// private static OssSettingOptions GetOssSetting() { var setting = App.GetConfig("OssSetting", true); var configurationIncomplete = setting == null || string.IsNullOrWhiteSpace(setting.AccessKeyID) || string.IsNullOrWhiteSpace(setting.AccessKeySecret) || string.IsNullOrWhiteSpace(setting.Bucket) || string.IsNullOrWhiteSpace(setting.EndPoint); if (configurationIncomplete) throw Oops.Bah("阿里云 OSS 配置不完整"); return setting; } /// /// 获取文件大小 /// /// /// private string GetSizeInfo(long fileSizeKb) { var b = fileSizeKb * 1024; const int mb = 1024 * 1024; const int kb = 1024; if (b / mb >= 1) { return Math.Round(b / (float)mb, 2) + "MB"; } if (b / kb >= 1) { return Math.Round(b / (float)kb, 2) + "KB"; } if (b == 0) { return "0B"; } return null; } /// /// 判断是否是图片 /// /// 后缀名 /// private bool IsPic(string suffix) { //图片后缀名列表 var pics = new[] { ".png", ".bmp", ".gif", ".jpg", ".jpeg", ".psd" }; if (pics.Contains(suffix)) return true; return false; } #endregion 方法 }