using System.Security.Cryptography; using System.Text; using System.Web; using JNPF.Common.Captcha.General; using JNPF.Common.Configuration; using JNPF.Common.Core.Manager; using JNPF.Common.Core.Manager.Files; using JNPF.Common.Enums; using JNPF.Common.Extension; using JNPF.Common.Manager; using JNPF.Common.Models; using JNPF.Common.Options; using JNPF.Common.Security; using JNPF.DataEncryption; using JNPF.DependencyInjection; using JNPF.DynamicApiController; using JNPF.FriendlyException; using JNPF.Logging.Attributes; using JNPF.RemoteRequest.Extensions; using JNPF.Systems.Interfaces.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; namespace JNPF.Systems.Common; /// /// 业务实现:通用控制器. /// [ApiDescriptionSettings(Tag = "Common", Name = "File", Order = 161)] [Route("api/[controller]")] [AllowAnonymous] [IgnoreLog] public class FileService : IFileService, IDynamicApiController, ITransient { private readonly AppOptions _appOptions; /// /// 验证码处理程序. /// private readonly IGeneralCaptcha _captchaHandler; /// /// 用户管理. /// private readonly IUserManager _userManager; /// /// 文件服务. /// private readonly IFileManager _fileManager; /// /// 缓存服务. /// private readonly ICacheManager _cacheManager; /// /// 初始化一个类型的新实例. /// public FileService( IOptions appOptions, IGeneralCaptcha captchaHandler, IUserManager userManager, ICacheManager cacheManager, IFileManager fileManager) { _appOptions = appOptions.Value; _captchaHandler = captchaHandler; _userManager = userManager; _cacheManager = cacheManager; _fileManager = fileManager; } #region GET /// /// 上传文件预览. /// /// [HttpGet("Uploader/Preview")] public async Task Preview(string fileName) { string[]? typeList = new string[] { "doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf", "jpg", "jpeg", "gif", "png", "bmp" }; string? type = fileName.Split('.').LastOrDefault(); if (typeList.Contains(type)) { if (fileName.IsNotEmptyOrNull()) { string previewUrl = string.Empty; switch (_appOptions.PreviewType) { case PreviewType.kkfile: previewUrl = KKFileUploaderPreview(fileName); break; case PreviewType.yozo: previewUrl = await YoZoUploaderPreview(fileName, 5, 1); break; } return previewUrl; } else { throw Oops.Oh(ErrorCode.D8000); } } else { throw Oops.Oh(ErrorCode.D1802); } } /// /// 生成图片链接. /// /// 图片类型. /// 注意 后缀名前端故意把 .替换@ . /// [HttpGet("Image/{type}/{fileName}")] public async Task GetImg(string type, string fileName) { string? filePath = Path.Combine(GetPathByType(type), fileName.Replace("@", ".")); return await _fileManager.DownloadFileByType(filePath, fileName); } /// /// 生成大屏图片链接. /// /// 图片类型. /// 注意 后缀名前端故意把 .替换@ . /// [HttpGet("VisusalImg/{type}/{fileName}")] public async Task GetScreenImg(string type, string fileName) { string filePath = Path.Combine(GetPathByType(type), type, fileName.Replace("@", ".")); return await _fileManager.DownloadFileByType(filePath, fileName); } /// /// 获取图形验证码. /// /// 时间戳. /// [HttpGet("ImageCode/{timestamp}")] [NonUnify] public async Task GetCode(string timestamp) { return new FileContentResult(await _captchaHandler.CreateCaptchaImage(timestamp, 114, 32), "image/jpeg"); } /// /// 下载. /// /// [HttpGet("down/{fileName}")] public async Task FileDown(string fileName) { string? systemFilePath = Path.Combine(FileVariable.SystemFilePath, fileName); var fileStreamResult = await _fileManager.DownloadFileByType(systemFilePath, fileName); byte[] bytes = new byte[fileStreamResult.FileStream.Length]; fileStreamResult.FileStream.Read(bytes, 0, bytes.Length); fileStreamResult.FileStream.Close(); var httpContext = App.HttpContext; httpContext.Response.ContentType = "application/octet-stream"; httpContext.Response.Headers.Add("Content-Disposition", "attachment;filename=" + HttpUtility.UrlEncode(fileName, Encoding.UTF8)); httpContext.Response.Headers.Add("Content-Length", bytes.Length.ToString()); httpContext.Response.Body.WriteAsync(bytes); httpContext.Response.Body.Flush(); httpContext.Response.Body.Close(); } #region 下载附件 /// /// 获取下载文件链接. /// /// 图片类型. /// 文件名称. /// [HttpGet("Download/{type}/{fileName}")] public dynamic DownloadUrl(string type, string fileName) { string? url = string.Format("{0}|{1}|{2}", _userManager.UserId, fileName, type); string? encryptStr = DESCEncryption.Encrypt(url, "JNPF"); _cacheManager.Set(fileName, string.Empty); return new { name = fileName, url = string.Format("/api/file/Download?encryption={0}", encryptStr) }; } /// /// 下载文件链接. /// [HttpGet("Download")] public async Task DownloadFile([FromQuery] string encryption, [FromQuery] string name) { string decryptStr = DESCEncryption.Decrypt(encryption, "JNPF"); List paramsList = decryptStr.Split("|").ToList(); if (paramsList.Count > 0) { string fileName = paramsList.Count > 1 ? paramsList[1] : string.Empty; if (_cacheManager.Exists(fileName)) { _cacheManager.Del(fileName); } else { throw Oops.Oh(ErrorCode.D1805); } string type = paramsList.Count > 2 ? paramsList[2] : string.Empty; string filePath = Path.Combine(GetPathByType(type), fileName.Replace("@", ".")); string fileDownloadName = name.IsNullOrEmpty() ? fileName : name; return await _fileManager.DownloadFileByType(filePath, fileDownloadName); } else { throw Oops.Oh(ErrorCode.D8000); } } /// /// App启动信息. /// [HttpGet("AppStartInfo/{appName}")] public async Task AppStartInfo(string appName) { return new { appVersion = KeyVariable.AppVersion, appUpdateContent = KeyVariable.AppUpdateContent }; } #endregion /// /// 分片上传获取. /// /// 请求参数. /// [HttpGet("chunk")] public async Task CheckChunk([FromQuery] ChunkModel input) { try { if (!AllowFileType(input.extension, input.extension)) throw Oops.Oh(ErrorCode.D1800); string path = GetPathByType(string.Empty); string filePath = Path.Combine(path, input.identifier); var chunkFiles = FileHelper.GetAllFiles(filePath); List existsChunk = chunkFiles.FindAll(x => !FileHelper.GetFileType(x).Equals("tmp")) .Select(x => x.FullName.Replace(input.identifier + "-", string.Empty).ParseToInt()).ToList(); return new { chunkNumbers = existsChunk, merge = false }; } catch (Exception ex) { throw; } } #endregion #region POST /// /// 上传文件/图片. /// /// [HttpPost("Uploader/{type}")] [AllowAnonymous] [IgnoreLog] public async Task Uploader(string type, IFormFile file) { string? fileType = Path.GetExtension(file.FileName).Replace(".", string.Empty); if (!AllowFileType(fileType, type)) throw Oops.Oh(ErrorCode.D1800); string filePath = GetPathByType(type); string fileName = string.Format("{0}{1}{2}", DateTime.Now.ToString("yyyyMMdd"), RandomExtensions.NextLetterAndNumberString(new Random(), 5), Path.GetExtension(file.FileName)); var stream = file.OpenReadStream(); await _fileManager.UploadFileByType(stream, filePath, fileName); return new { name = fileName, url = string.Format("/api/File/Image/{0}/{1}", type, fileName), fileSize = file.Length, fileExtension = fileType }; } /// /// 上传图片. /// /// [HttpPost("Uploader/userAvatar")] [AllowAnonymous] [IgnoreLog] public async Task UploadImage(IFormFile file) { string? ImgType = Path.GetExtension(file.FileName).Replace(".", string.Empty); if (!this.AllowImageType(ImgType)) throw Oops.Oh(ErrorCode.D5013); string? filePath = FileVariable.UserAvatarFilePath; string? fileName = string.Format("{0}{1}{2}", DateTime.Now.ToString("yyyyMMdd"), RandomExtensions.NextLetterAndNumberString(new Random(), 5), Path.GetExtension(file.FileName)); var stream = file.OpenReadStream(); await _fileManager.UploadFileByType(stream, filePath, fileName); return new FileControlsModel { name = fileName, url = string.Format("/api/file/Image/userAvatar/{0}", fileName), fileSize = file.Length, fileExtension = ImgType }; } /// /// 分片上传附件. /// /// /// [HttpPost("chunk")] [AllowAnonymous] [IgnoreLog] public async Task UploadChunk([FromForm] ChunkModel input) { if (!AllowFileType(input.extension, input.extension)) throw Oops.Oh(ErrorCode.D1800); return await _fileManager.UploadChunk(input); } /// /// 分片组装. /// /// /// [HttpPost("merge")] [AllowAnonymous] [IgnoreLog] public async Task Merge([FromForm] ChunkModel input) { return await _fileManager.Merge(input); } #endregion #region PublicMethod #region 多种存储文件 /// /// 根据存储类型上传文件. /// /// 上传文件地址. /// 保存文件夹. /// 新文件名. /// [NonAction] public async Task UploadFileByType(string uploadFilePath, string directoryPath, string fileName) { FileStream? file = new FileStream(uploadFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); await _fileManager.UploadFileByType(file, directoryPath, fileName); } #endregion /// /// 根据类型获取文件存储路径. /// /// 文件类型. /// [NonAction] public string GetPathByType(string type) { return _fileManager.GetPathByType(type); } #region kkfile 文件预览 /// /// KKFile 文件预览. /// /// 文件名称. /// public string KKFileUploaderPreview(string fileName) { var domain = App.Configuration["JNPF_APP:Domain"]; var filePath = (domain + "/api/File/down/" + fileName).ToBase64String(); var kkFileDoMain = App.Configuration["JNPF_APP:KKFileDomain"]; var kkurl = kkFileDoMain + "/onlinePreview?url="; return kkurl + filePath; } #endregion #region YoZo 生成 sign 方法 /// /// 调用YoZo 文件预览. /// /// 文件名. /// 最多请求次数. /// 当前请求次数. /// public async Task YoZoUploaderPreview(string fileName, int maxNumber, int number) { string domain = _appOptions.YOZO.Domain; string uploadAPI = _appOptions.YOZO.UploadAPI; string downloadAPI = _appOptions.YOZO.DownloadAPI; string yozoAppId = _appOptions.YOZO.AppId; string yozoAppKey = _appOptions.YOZO.AppKey; string outputFilePath = string.Format("{0}/api/File/Image/annex/{1}", domain, fileName); Dictionary dic = new Dictionary(); dic.Add("fileUrl", new string[] { outputFilePath }); dic.Add("appId", new string[] { yozoAppId }); string? sign = generateSign(yozoAppKey, dic); uploadAPI = string.Format(uploadAPI, outputFilePath, yozoAppId, sign); string? resStr = await uploadAPI.PostAsStringAsync(); if (resStr.IsNotEmptyOrNull()) { Dictionary? result = resStr.ToObject>(); if (result.ContainsKey("data")) { Dictionary? data = result["data"].ToObject>(); if (data != null) { string? fileVersionId = data.ContainsKey("fileVersionId") ? data["fileVersionId"].ToString() : string.Empty; #region 生成签名sign dic = new Dictionary(); dic.Add("fileVersionId", new string[] { fileVersionId }); dic.Add("appId", new string[] { yozoAppId }); sign = generateSign(yozoAppKey, dic); #endregion return string.Format(downloadAPI, fileVersionId, yozoAppId, sign); } else { return await YoZoUploaderPreview(fileName, maxNumber, number + 1); } } else { if (number >= maxNumber) return string.Empty; else return await YoZoUploaderPreview(fileName, maxNumber, number + 1); } } else { if (number >= maxNumber) return string.Empty; else return await YoZoUploaderPreview(fileName, maxNumber, number + 1); } } #endregion #endregion #region PrivateMethod /// /// 生成令牌. /// /// 签名. /// 参数集合. /// private string generateSign(string secret, Dictionary paramMap) { string fullParamStr = uniqSortParams(paramMap); return HmacSHA256(fullParamStr, secret); } /// /// uniq类型参数. /// /// /// private string uniqSortParams(Dictionary paramMap) { paramMap.Remove("sign"); paramMap = paramMap.OrderBy(o => o.Key).ToDictionary(o => o.Key, p => p.Value); StringBuilder strB = new StringBuilder(); foreach (KeyValuePair kvp in paramMap) { string key = kvp.Key; string[] value = kvp.Value; if (value.Length > 0) { Array.Sort(value); foreach (string temp in value) { strB.Append(key).Append("=").Append(temp); } } else { strB.Append(key).Append("="); } } return strB.ToString(); } /// /// 加密. /// /// /// /// private string HmacSHA256(string data, string key) { string signRet = string.Empty; using (HMACSHA256 mac = new HMACSHA256(Encoding.UTF8.GetBytes(key))) { byte[] hash = mac.ComputeHash(Encoding.UTF8.GetBytes(data)); signRet = hash.ToHexString(); } return signRet; } /// /// 允许文件类型. /// /// 文件后缀名. /// 文件类型. /// private bool AllowFileType(string fileExtension, string type) { List? allowExtension = KeyVariable.AllowUploadFileType; if (fileExtension.IsNullOrEmpty() || type.IsNullOrEmpty()) return false; if (type.Equals("weixin")) allowExtension = KeyVariable.WeChatUploadFileType; return allowExtension.Any(a => a == fileExtension.ToLower()); } /// /// 允许文件类型. /// /// 文件后缀名. /// private bool AllowImageType(string fileExtension) { return KeyVariable.AllowImageType.Any(a => a == fileExtension.ToLower()); } #endregion }