using System.IO.Compression;
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 fileDownloadUrl)
{
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, fileDownloadUrl);
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, [FromQuery] string type)
{
string? systemFilePath = Path.Combine(FileVariable.SystemFilePath, fileName);
if (type.IsNotEmptyOrNull())
{
systemFilePath = Path.Combine(_fileManager.GetPathByType(type), 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) };
}
///
/// 全部下载.
///
/// 图片类型.
/// 文件名称.
///
[HttpPost("PackDownload/{type}")]
public async Task DownloadAll(string type, [FromBody] List input)
{
var fileName = RandomExtensions.NextLetterAndNumberString(new Random(), 7);
//临时目录
string directoryPath = Path.Combine(App.GetConfig("JNPF_App", true).SystemPath, "TemporaryFile", fileName);
Directory.CreateDirectory(directoryPath);
foreach (var item in input)
{
string filePath = Path.Combine(GetPathByType(type), item.fileId.Replace("@", "."));
await _fileManager.CopyFile(filePath, Path.Combine(directoryPath, item.fileName));
}
// 压缩文件
string downloadPath = directoryPath + ".zip";
// 判断是否存在同名称文件
if (File.Exists(downloadPath))
File.Delete(downloadPath);
ZipFile.CreateFromDirectory(directoryPath, downloadPath);
if (!App.Configuration["OSS:Provider"].Equals("Invalid"))
await UploadFileByType(downloadPath, "SystemPath", string.Format("文件{0}.zip", fileName));
var downloadFileName = string.Format("{0}|{1}.zip|TemporaryFile", _userManager.UserId, fileName);
_cacheManager.Set(fileName + ".zip", string.Empty);
return new { downloadName = string.Format("文件{0}.zip", fileName), downloadVo = new { name = fileName, url = "/api/File/Download?encryption=" + DESCEncryption.Encrypt(downloadFileName, "JNPF") } };
}
///
/// 下载文件链接.
///
[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, [FromForm] ChunkModel input)
{
string? fileType = Path.GetExtension(input.file.FileName).Replace(".", string.Empty);
if (!AllowFileType(fileType, type))
throw Oops.Oh(ErrorCode.D1800);
string saveFileName = string.Format("{0}{1}{2}", DateTime.Now.ToString("yyyyMMdd"), RandomExtensions.NextLetterAndNumberString(new Random(), 5), Path.GetExtension(input.file.FileName));
var stream = input.file.OpenReadStream();
input.type = type;
_fileManager.GetChunkModel(input, saveFileName);
await _fileManager.UploadFileByType(stream, input.folder, saveFileName);
return new FileControlsModel { name = input.fileName, url = string.Format("/api/File/Image/{0}/{1}", type, input.fileName), fileExtension = fileType, fileSize = input.file.Length, fileName = input.fileName };
}
///
/// 上传图片.
///
///
[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, string fileDownloadUrl)
{
var domain = App.Configuration["JNPF_APP:Domain"];
var filePath = (domain + "/api/File/down/" + fileName).ToBase64String();
if (fileDownloadUrl.IsNotEmptyOrNull())
{
var list = fileDownloadUrl.Split('/');
var type = list.Length > 4 ? list[4] : string.Empty;
filePath = string.Format("{0}{1}{2}?type={3}", domain, "/api/File/down/", fileName, type).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
}