Эх сурвалжийг харах

!1114 添加SSH存储文件支持,上传文件添加文件表添加关联字段和类型
Merge pull request !1114 from 闫腾/next

zuohuaijun 1 жил өмнө
parent
commit
2318a47b58

+ 7 - 0
Admin.NET/Admin.NET.Application/Configuration/Upload.json

@@ -17,5 +17,12 @@
     "IsEnableHttps": false, // 是否启用HTTPS
     "IsEnableCache": true, // 是否启用缓存
     "Bucket": "admin.net"
+  },
+  "SSHProvider": {
+    "IsEnable": false, 
+    "Host": "127.0.0.1",
+    "Port": 8222,
+    "Username": "sshuser",
+    "Password": "Password.1"
   }
 }

+ 1 - 0
Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj

@@ -35,6 +35,7 @@
     <PackageReference Include="SKIT.FlurlHttpClient.Wechat.Api" Version="3.1.0" />
     <PackageReference Include="SKIT.FlurlHttpClient.Wechat.TenpayV3" Version="3.4.0" />
     <PackageReference Include="SqlSugarCore" Version="5.1.4.155" />
+    <PackageReference Include="SSH.NET" Version="2024.0.0" />
     <PackageReference Include="System.Linq.Dynamic.Core" Version="1.3.14" />
     <PackageReference Include="UAParser" Version="3.1.47" />
     <PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />

+ 31 - 2
Admin.NET/Admin.NET.Core/Entity/SysFile.cs

@@ -45,8 +45,8 @@ public partial class SysFile : EntityBase
     /// <summary>
     /// 存储路径
     /// </summary>
-    [SugarColumn(ColumnDescription = "存储路径", Length = 128)]
-    [MaxLength(128)]
+    [SugarColumn(ColumnDescription = "存储路径", Length = 512)]
+    [MaxLength(512)]
     public string? FilePath { get; set; }
 
     /// <summary>
@@ -76,4 +76,33 @@ public partial class SysFile : EntityBase
     [SugarColumn(ColumnDescription = "文件MD5", Length = 128)]
     [MaxLength(128)]
     public string? FileMd5 { get; set; }
+
+    
+    /// <summary>
+    /// 关联对象名称(如子对象)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "关联对象名称", Length = 100)]
+    [MaxLength(100)]
+    public string RelationName { get; set; }
+
+
+    /// <summary>
+    /// 关联对象ID
+    /// </summary> 
+    [SugarColumn(ColumnDescription = "关联对象ID" )] 
+    public long? RelationId { get; set; }
+ 
+
+    /// <summary>
+    /// 所属ID(如主对象)
+    /// </summary> 
+    [SugarColumn(ColumnDescription = "所属ID" )] 
+    public long? BelongId { get; set; }
+
+    /// <summary>
+    /// 文件类别
+    /// </summary>
+    [SugarColumn(ColumnDescription = "文件类别", Length = 100)]
+    [MaxLength(100)]
+    public string FileType { get; set; } 
 }

+ 110 - 1
Admin.NET/Admin.NET.Core/Service/File/Dto/FileInput.cs

@@ -12,7 +12,11 @@ public class FileInput : BaseIdInput
     /// 文件名称
     /// </summary>
     public string FileName { get; set; }
-
+    /// <summary>
+    /// 文件类型
+    /// </summary>
+    public string FileType { get; set; }
+    
     /// <summary>
     /// 文件Url
     /// </summary>
@@ -40,6 +44,7 @@ public class PageFileInput : BasePageInput
 public class DeleteFileInput : BaseIdInput
 {
 }
+ 
 
 public class UploadFileFromBase64Input
 {
@@ -62,4 +67,108 @@ public class UploadFileFromBase64Input
     /// 保存路径
     /// </summary>
     public string Path { get; set; }
+    /// <summary>
+    /// 文件类型
+    /// </summary>
+    public string FileType { get; set; }
+
+}
+
+/// <summary>
+/// 上传文件
+/// </summary>
+public class FileUploadInput
+{
+    [Required]
+    public IFormFile File { get; set; }
+    public string FileType { get; set; }
+    public string Path { get; set; }
+}
+
+/// <summary>
+/// 查询关联查询输入
+/// </summary>
+public class RelationQueryInput
+{
+    /// <summary>
+    /// 关联对象名称
+    /// </summary>
+    public string RelationName { get; set; }
+    /// <summary>
+    /// 关联对象Id
+    /// </summary>
+    public long? RelationId { get; set; }
+    /// <summary>
+    /// 文件,多个以“,”分割
+    /// </summary>
+    public string FileTypes { get; set; } 
+
+    /// <summary>
+    /// 所属ID
+    /// </summary>
+    public long? BelongId { get; set; } 
+
+    public string[] GetFileTypeBS()
+    {
+        return FileTypes.Split(',');
+    }
+}
+public class FileOutput
+{
+    /// <summary>
+    /// Id
+    /// </summary>
+    public long Id { get; set; } 
+    /// <summary>
+    /// 名称
+    /// </summary>
+    public string Name { get; set; }
+
+    /// <summary>
+    /// URL
+    /// </summary>
+    public string Url { get; set; }
+
+    /// <summary>
+    /// 大小
+    /// </summary>
+    public string SizeKb { get; set; }
+
+    /// <summary>
+    /// 后缀
+    /// </summary>
+    public string Suffix { get; set; }
+
+    /// <summary>
+    /// 路径
+    /// </summary>
+    public string FilePath { get; set; }
+
+    /// <summary>
+    /// 文件类别
+    /// </summary>
+    public string FileType { get; set; }
+
+    /// <summary>
+    /// 上传人
+    /// </summary>
+    public string CreateUserName { get; set; }
+
+    /// <summary>
+    /// 上传时间
+    /// </summary>
+    public DateTime? CreateTime { get; set; }
+    /// <summary>
+    /// 关联对象名称
+    /// </summary>
+    public string RelationName { get; set; }
+    /// <summary>
+    /// 关联对象Id
+    /// </summary>
+    public long? RelationId { get; set; } 
+    /// <summary>
+    /// 所属ID
+    /// </summary>
+    public long? BelongId { get; set; }
+
 }

+ 139 - 16
Admin.NET/Admin.NET.Core/Service/File/SysFileService.cs

@@ -6,7 +6,9 @@
 
 using Aliyun.OSS.Util;
 using Furion.VirtualFileServer;
+using Microsoft.AspNetCore.Components.Forms;
 using OnceMi.AspNetCore.OSS;
+using System.IO;
 
 namespace Admin.NET.Core.Service;
 
@@ -56,13 +58,13 @@ public class SysFileService : IDynamicApiController, ITransient
     /// <summary>
     /// 上传文件 🔖
     /// </summary>
-    /// <param name="file"></param>
-    /// <param name="path"></param>
+    /// <param name="input"></param> 
     /// <returns></returns>
     [DisplayName("上传文件")]
-    public async Task<SysFile> UploadFile([Required] IFormFile file, [FromQuery] string? path)
+    [HttpPost]
+    public async Task<SysFile> UploadFile([FromForm]FileUploadInput input)
     {
-        return await HandleUploadFile(file, path);
+        return await HandleUploadFile(input.File, input.Path, filetype: input.FileType);
     }
 
     /// <summary>
@@ -72,8 +74,9 @@ public class SysFileService : IDynamicApiController, ITransient
     /// <param name="fileName"></param>
     /// <param name="contentType"></param>
     /// <param name="path"></param>
+    /// <param name="fileType"></param>
     /// <returns></returns>
-    private async Task<SysFile> UploadFileFromBase64(string strBase64, string fileName, string contentType, string? path)
+    private async Task<SysFile> UploadFileFromBase64(string strBase64, string fileName, string contentType, string? path,string? fileType)
     {
         byte[] fileData = Convert.FromBase64String(strBase64);
         var ms = new MemoryStream();
@@ -88,7 +91,7 @@ public class SysFileService : IDynamicApiController, ITransient
             Headers = new HeaderDictionary(),
             ContentType = contentType
         };
-        return await UploadFile(formFile, path);
+        return await UploadFile(new FileUploadInput {File= formFile, Path= path,FileType= fileType });
     }
 
     /// <summary>
@@ -100,7 +103,7 @@ public class SysFileService : IDynamicApiController, ITransient
     [HttpPost]
     public async Task<SysFile> UploadFileFromBase64(UploadFileFromBase64Input input)
     {
-        return await UploadFileFromBase64(input.FileDataBase64, input.FileName, input.ContentType, input.Path);
+        return await UploadFileFromBase64(input.FileDataBase64, input.FileName, input.ContentType, input.Path,input.FileType);
     }
 
     /// <summary>
@@ -114,7 +117,7 @@ public class SysFileService : IDynamicApiController, ITransient
         var filelist = new List<SysFile>();
         foreach (var file in files)
         {
-            filelist.Add(await UploadFile(file, ""));
+            filelist.Add(await UploadFile(new FileUploadInput {File=file}));
         }
         return filelist;
     }
@@ -129,19 +132,58 @@ public class SysFileService : IDynamicApiController, ITransient
     {
         var file = input.Id > 0 ? await GetFile(input) : await _sysFileRep.GetFirstAsync(u => u.Url == input.Url);
         var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8"));
-        if (_OSSProviderOptions.IsEnable)
+
+       if (_OSSProviderOptions.IsEnable)
         {
             var filePath = string.Concat(file.FilePath, "/", file.Id.ToString() + file.Suffix);
             var stream = await (await _OSSService.PresignedGetObjectAsync(file.BucketName.ToString(), filePath, 5)).GetAsStreamAsync();
             return new FileStreamResult(stream.Stream, "application/octet-stream") { FileDownloadName = fileName + file.Suffix };
         }
-        else
+        else if (App.Configuration["SSHProvider:IsEnable"].ToBoolean())
+        {
+            var fullPath = string.Concat(file.FilePath, "/", file.Id+ file.Suffix);
+            using (SSHHelper helper = new SSHHelper(App.Configuration["SSHProvider:Host"],
+               App.Configuration["SSHProvider:Port"].ToInt(), App.Configuration["SSHProvider:Username"], App.Configuration["SSHProvider:Password"]))
+            { 
+                return new FileStreamResult(helper.OpenRead(fullPath), "application/octet-stream") { FileDownloadName = fileName + file.Suffix };
+            }
+        }
+        else 
         {
             var filePath = Path.Combine(file.FilePath, file.Id.ToString() + file.Suffix);
             var path = Path.Combine(App.WebHostEnvironment.WebRootPath, filePath);
             return new FileStreamResult(new FileStream(path, FileMode.Open), "application/octet-stream") { FileDownloadName = fileName + file.Suffix };
         }
     }
+    [AllowAnonymous]
+    [HttpGet("/api/sysFile/Preview/{id}")]
+    public async Task<IActionResult> Preview([FromRoute] long Id)
+    {
+        var file = await GetFile(new FileInput { Id = Id });
+        var fileName = HttpUtility.UrlEncode(file.FileName, Encoding.GetEncoding("UTF-8"));
+
+        if (_OSSProviderOptions.IsEnable)
+        {
+            var filePath = string.Concat(file.FilePath, "/", file.Id.ToString() + file.Suffix);
+            var stream = await (await _OSSService.PresignedGetObjectAsync(file.BucketName.ToString(), filePath, 5)).GetAsStreamAsync();
+            return new FileStreamResult(stream.Stream, "application/octet-stream") ;
+        }
+        else if (App.Configuration["SSHProvider:IsEnable"].ToBoolean())
+        {
+            var fullPath = string.Concat(file.FilePath, "/", file.Id + file.Suffix);
+            using (SSHHelper helper = new SSHHelper(App.Configuration["SSHProvider:Host"],
+               App.Configuration["SSHProvider:Port"].ToInt(), App.Configuration["SSHProvider:Username"], App.Configuration["SSHProvider:Password"]))
+            {
+                return new FileStreamResult(helper.OpenRead(fullPath), "application/octet-stream") ;
+            }
+        }
+        else
+        {
+            var filePath = Path.Combine(file.FilePath, file.Id.ToString() + file.Suffix);
+            var path = Path.Combine(App.WebHostEnvironment.WebRootPath, filePath);
+            return new FileStreamResult(new FileStream(path, FileMode.Open), "application/octet-stream") ;
+        }
+    }
 
     /// <summary>
     /// 下载指定文件Base64格式 🔖
@@ -151,7 +193,7 @@ public class SysFileService : IDynamicApiController, ITransient
     [AllowAnonymous]
     [DisplayName("下载指定文件Base64格式")]
     public async Task<string> DownloadFileBase64([FromBody] string url)
-    {
+    { 
         if (_OSSProviderOptions.IsEnable)
         {
             using var httpClient = new HttpClient();
@@ -167,7 +209,16 @@ public class SysFileService : IDynamicApiController, ITransient
                 throw new HttpRequestException($"Request failed with status code: {response.StatusCode}");
             }
         }
-        else
+        else if (App.Configuration["SSHProvider:IsEnable"].ToBoolean())
+        {
+            var sysFile = await _sysFileRep.GetFirstAsync(u => u.Url == url) ?? throw Oops.Oh($"文件不存在");
+            using (SSHHelper helper = new SSHHelper(App.Configuration["SSHProvider:Host"],
+               App.Configuration["SSHProvider:Port"].ToInt(), App.Configuration["SSHProvider:Username"], App.Configuration["SSHProvider:Password"]))
+            { 
+                return Convert.ToBase64String(helper.ReadAllBytes(sysFile.FilePath));
+            }
+        }
+        else 
         {
             var sysFile = await _sysFileRep.GetFirstAsync(u => u.Url == url) ?? throw Oops.Oh($"文件不存在");
             var filePath = Path.Combine(App.WebHostEnvironment.WebRootPath, sysFile.FilePath);
@@ -200,6 +251,15 @@ public class SysFileService : IDynamicApiController, ITransient
             {
                 await _OSSService.RemoveObjectAsync(file.BucketName.ToString(), string.Concat(file.FilePath, "/", $"{input.Id}{file.Suffix}"));
             }
+            else if (App.Configuration["SSHProvider:IsEnable"].ToBoolean())
+            {
+                var fullPath = string.Concat(file.FilePath, "/", file.Id + file.Suffix);
+                using (SSHHelper helper = new SSHHelper(App.Configuration["SSHProvider:Host"],
+                   App.Configuration["SSHProvider:Port"].ToInt(), App.Configuration["SSHProvider:Username"], App.Configuration["SSHProvider:Password"]))
+                {
+                    helper.DeleteFile(fullPath);
+                }
+            }
             else
             {
                 var filePath = Path.Combine(App.WebHostEnvironment.WebRootPath, file.FilePath, input.Id.ToString() + file.Suffix);
@@ -221,7 +281,7 @@ public class SysFileService : IDynamicApiController, ITransient
         var isExist = await _sysFileRep.IsAnyAsync(u => u.Id == input.Id);
         if (!isExist) throw Oops.Oh(ErrorCodeEnum.D8000);
 
-        await _sysFileRep.UpdateAsync(u => new SysFile() { FileName = input.FileName }, u => u.Id == input.Id);
+        await _sysFileRep.UpdateAsync(u => new SysFile() { FileName = input.FileName,FileType=input.FileType }, u => u.Id == input.Id);
     }
 
     /// <summary>
@@ -242,7 +302,7 @@ public class SysFileService : IDynamicApiController, ITransient
     /// <param name="savePath">路径</param>
     /// <param name="allowSuffix">允许格式:.jpg.png.gif.tif.bmp</param>
     /// <returns></returns>
-    private async Task<SysFile> HandleUploadFile(IFormFile file, string savePath, string allowSuffix = "")
+    private async Task<SysFile> HandleUploadFile(IFormFile file, string savePath, string allowSuffix = "",string filetype = "")
     {
         if (file == null) throw Oops.Oh(ErrorCodeEnum.D8000);
 
@@ -305,10 +365,11 @@ public class SysFileService : IDynamicApiController, ITransient
             SizeKb = sizeKb.ToString(),
             FilePath = path,
             FileMd5 = fileMd5,
+            FileType= filetype
         };
 
         var finalName = newFile.Id + suffix; // 文件最终名称
-        if (_OSSProviderOptions.IsEnable)
+       if (_OSSProviderOptions.IsEnable)
         {
             newFile.Provider = Enum.GetName(_OSSProviderOptions.Provider);
             var filePath = string.Concat(path, "/", finalName);
@@ -329,7 +390,16 @@ public class SysFileService : IDynamicApiController, ITransient
                     break;
             }
         }
-        else
+        else if (App.Configuration["SSHProvider:IsEnable"].ToBoolean())
+        {
+            var fullPath = string.Concat( path.StartsWith("/")?path:"/"+path, "/", finalName);
+            using (SSHHelper helper = new SSHHelper(App.Configuration["SSHProvider:Host"],
+               App.Configuration["SSHProvider:Port"].ToInt(), App.Configuration["SSHProvider:Username"], App.Configuration["SSHProvider:Password"])) {
+
+                helper.UploadFile(file.OpenReadStream(), fullPath);
+            }
+        }
+        else 
         {
             newFile.Provider = ""; // 本地存储 Provider 显示为空
             var filePath = Path.Combine(App.WebHostEnvironment.WebRootPath, path);
@@ -417,4 +487,57 @@ public class SysFileService : IDynamicApiController, ITransient
         await sysUserRep.UpdateAsync(u => new SysUser() { Signature = sysFile.Url }, u => u.Id == user.Id);
         return sysFile;
     }
+
+    /// <summary>
+    /// 修改附件关联对象
+    /// </summary>
+    /// <param name="ids"></param>
+    /// <param name="relationName"></param>
+    /// <param name="relationId"></param>
+    /// <param name="belongId"></param>
+    /// <returns></returns>  
+    [NonAction]
+    public async Task<int> UpdateRelation(List<long> ids, string relationName, long relationId, long belongId = 0)
+    {
+        if (ids == null || ids.Count == 0)
+            return 0; 
+        return await _sysFileRep.AsUpdateable()
+              .SetColumns(m => m.RelationName == relationName)
+              .SetColumns(m => m.RelationId == relationId)
+              .SetColumns(m => m.BelongId == belongId)
+             .Where(m => ids.Contains(m.Id))
+             .ExecuteCommandAsync();
+    }
+
+    /// <summary>
+    /// 根据关联查询附件
+    /// </summary>
+    /// <param name="input"></param> 
+    /// <returns></returns>
+    /// <exception cref="ArgumentNullException"></exception>  
+    public async Task<List<FileOutput>> GetRelationFiles(RelationQueryInput input)
+    {
+        return await _sysFileRep.AsQueryable()
+           .Where(m => !m.IsDelete)
+           .WhereIF(input.RelationId.HasValue && input.RelationId > 0, m => m.RelationId == input.RelationId)
+           .WhereIF(input.BelongId.HasValue && input.BelongId > 0, m => m.BelongId == input.BelongId.Value)
+           .WhereIF(!string.IsNullOrWhiteSpace(input.RelationName), m => m.RelationName == input.RelationName) 
+           .WhereIF(!string.IsNullOrWhiteSpace(input.FileTypes), m => input.GetFileTypeBS().Contains(m.FileType))
+            .Select(m => new FileOutput
+            {
+                Id = m.Id,
+                FileType = m.FileType,
+                Name = m.FileName,
+                RelationId = m.RelationId,
+                BelongId = m.BelongId,
+                FilePath = m.FilePath,
+                SizeKb = m.SizeKb,
+                Suffix = m.Suffix,
+                RelationName = m.RelationName,
+                Url = SqlFunc.MergeString("/api/sysFile/Preview/", m.Id.ToString()),
+                CreateUserName = m.CreateUserName,
+                CreateTime = m.CreateTime,
+            })
+           .ToListAsync();
+    }
 }

+ 199 - 0
Admin.NET/Admin.NET.Core/Util/SSHHelper.cs

@@ -0,0 +1,199 @@
+
+using Microsoft.Extensions.Logging;
+using Renci.SshNet;
+
+namespace Admin.NET.Core
+{
+    /// <summary>
+    /// SSH / Sftp Helper
+    /// </summary>
+    public class SSHHelper : IDisposable
+    {
+        private SftpClient sftp;
+        public SSHHelper(string host, int port, string user, string password)
+        {
+            sftp = new SftpClient(host, port, user, password);
+        }
+        public bool Exists(string ftpFileName)
+        {
+            Connect();
+            return sftp.Exists(ftpFileName);
+
+        }
+
+        private void Connect()
+        {
+            if (!sftp.IsConnected)
+            {
+                sftp.Connect();
+            }
+        }
+
+        /// <summary>
+        /// 删除
+        /// </summary>
+        /// <param name="ftpFileName"></param>
+        public void DeleteFile(string ftpFileName)
+        { 
+            Connect();
+            sftp.DeleteFile(ftpFileName);
+
+        }
+        /// <summary>
+        /// 下载到指定目录
+        /// </summary>
+        /// <param name="ftpFileName"></param>
+        /// <param name="localFileName"></param>
+        public void DownloadFile(string ftpFileName, string localFileName)
+        {
+            Connect();
+            using (Stream fileStream = File.OpenWrite(localFileName))
+            {
+                sftp.DownloadFile(ftpFileName, fileStream);
+            }
+        }
+
+        /// <summary>
+        /// 读取字节
+        /// </summary>
+        /// <param name="ftpFileName"></param>
+        /// <returns></returns>
+        public byte[] ReadAllBytes(string ftpFileName)
+        {
+            Connect();
+            return sftp.ReadAllBytes(ftpFileName);
+        }
+        /// <summary>
+        /// 读取流
+        /// </summary>
+        /// <param name="path"></param>
+        /// <returns></returns>
+        public Stream OpenRead(string path)
+        {
+            return sftp.Open(path, FileMode.Open, FileAccess.Read);
+        }
+
+        /// <summary>
+        /// 继续下载
+        /// </summary>
+        /// <param name="ftpFileName"></param>
+        /// <param name="localFileName"></param>
+        public void DownloadFileWithResume(string ftpFileName, string localFileName)
+        {
+            DownloadFile(ftpFileName, localFileName);
+        }
+        /// <summary>
+        /// 重命名
+        /// </summary>
+        /// <param name="oldPath"></param>
+        /// <param name="newPath"></param>
+        public void RenameFile(string oldPath, string newPath)
+        {
+            sftp.RenameFile(oldPath, newPath);
+        }
+
+        /// <summary>
+        /// 指定目录下文件
+        /// </summary>
+        /// <param name="folder"></param>
+        /// <param name="filters"></param>
+        /// <returns></returns>
+        public List<string> GetFileList(string folder, IEnumerable<string> filters)
+        {
+            var files = new List<string>();
+            Connect();
+
+            var sftpFiles = sftp.ListDirectory(folder);
+
+            foreach (var file in sftpFiles)
+            {
+                if (file.IsRegularFile && filters.Any(f => file.Name.EndsWith(f)))
+                {
+                    files.Add(file.Name);
+                }
+            }
+
+            return files;
+        }
+        /// <summary>
+        /// 上传指定目录文件
+        /// </summary>
+        /// <param name="localFileName"></param>
+        /// <param name="ftpFileName"></param>
+        public void UploadFile(string localFileName, string ftpFileName)
+        {
+            Connect();
+            var dir = Path.GetDirectoryName(ftpFileName);
+            CreateDir(sftp, dir);
+            using (var fileStream = new FileStream(localFileName, FileMode.Open))
+            {
+                sftp.UploadFile(fileStream, ftpFileName);
+            }
+        }
+        /// <summary>
+        /// 上传字节
+        /// </summary>
+        /// <param name="bs"></param>
+        /// <param name="ftpFileName"></param>
+        public void UploadFile(byte[] bs, string ftpFileName)
+        {
+
+            Connect();
+            var dir = Path.GetDirectoryName(ftpFileName);
+            CreateDir(sftp, dir);
+
+            sftp.WriteAllBytes(ftpFileName, bs);
+        }
+
+        /// <summary>
+        /// 上传流
+        /// </summary>
+        /// <param name="fileStream"></param>
+        /// <param name="ftpFileName"></param>
+        public void UploadFile(Stream fileStream, string ftpFileName)
+        {
+            Connect();
+            var dir = Path.GetDirectoryName(ftpFileName);
+            CreateDir(sftp, dir);
+            sftp.UploadFile(fileStream, ftpFileName);
+            fileStream.Dispose();
+
+        }
+        /// <summary>
+        /// 创建目录
+        /// </summary>
+        /// <param name="sftp"></param>
+        /// <param name="dir"></param>
+        /// <exception cref="ArgumentNullException"></exception>
+        private void CreateDir(SftpClient sftp, string dir)
+        {
+            if (dir is null)
+            {
+                throw new ArgumentNullException(nameof(dir));
+            }
+            if (!sftp.Exists(dir))
+            {
+                var index = dir.LastIndexOfAny(new char[] { '/', '\\' });
+                if (index > 0)
+                {
+                    var p = dir.Substring(0, index);
+                    if (!sftp.Exists(p))
+                    {
+                        CreateDir(sftp, p);
+                    }
+                    sftp.CreateDirectory(dir);
+                }
+            }
+        }
+
+        public void Dispose()
+        {
+            if (sftp != null)
+            {
+                if (sftp.IsConnected)
+                    sftp.Disconnect();
+                sftp.Dispose();
+            }
+        }
+    }
+}

+ 12 - 7
Web/src/api-services/apis/sys-file-api.ts

@@ -380,7 +380,7 @@ export const SysFileApiAxiosParamCreator = function (configuration?: Configurati
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        apiSysFileUploadFilePostForm: async (file?: Blob, path?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        apiSysFileUploadFilePostForm: async (file?: Blob, path?: string, filetype?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             const localVarPath = `/api/sysFile/uploadFile`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, 'https://example.com');
@@ -411,6 +411,10 @@ export const SysFileApiAxiosParamCreator = function (configuration?: Configurati
                 localVarFormParams.append('file', file as any);
             }
 
+            if (filetype !== undefined) { 
+                localVarFormParams.append('filetype', filetype);
+            }
+
             localVarHeaderParameter['Content-Type'] = 'multipart/form-data';
             const query = new URLSearchParams(localVarUrlObj.search);
             for (const key in localVarQueryParameter) {
@@ -649,8 +653,8 @@ export const SysFileApiFp = function(configuration?: Configuration) {
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async apiSysFileUploadFilePostForm(file?: Blob, path?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultSysFile>>> {
-            const localVarAxiosArgs = await SysFileApiAxiosParamCreator(configuration).apiSysFileUploadFilePostForm(file, path, options);
+        async apiSysFileUploadFilePostForm(file?: Blob, path?: string, filetype?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultSysFile>>> {
+            const localVarAxiosArgs = await SysFileApiAxiosParamCreator(configuration).apiSysFileUploadFilePostForm(file, path,filetype, options);
             return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
                 const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
                 return axios.request(axiosRequestArgs);
@@ -768,11 +772,12 @@ export const SysFileApiFactory = function (configuration?: Configuration, basePa
          * @summary 上传文件 🔖
          * @param {Blob} [file] 
          * @param {string} [path] 
+         * @param {string} [filetype] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async apiSysFileUploadFilePostForm(file?: Blob, path?: string, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultSysFile>> {
-            return SysFileApiFp(configuration).apiSysFileUploadFilePostForm(file, path, options).then((request) => request(axios, basePath));
+        async apiSysFileUploadFilePostForm(file?: Blob, path?: string,filetype?:string, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultSysFile>> {
+            return SysFileApiFp(configuration).apiSysFileUploadFilePostForm(file, path,filetype, options).then((request) => request(axios, basePath));
         },
         /**
          * 
@@ -890,8 +895,8 @@ export class SysFileApi extends BaseAPI {
      * @throws {RequiredError}
      * @memberof SysFileApi
      */
-    public async apiSysFileUploadFilePostForm(file?: Blob, path?: string, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultSysFile>> {
-        return SysFileApiFp(this.configuration).apiSysFileUploadFilePostForm(file, path, options).then((request) => request(this.axios, this.basePath));
+    public async apiSysFileUploadFilePostForm(file?: Blob, path?: string, filetype?: string, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultSysFile>> {
+        return SysFileApiFp(this.configuration).apiSysFileUploadFilePostForm(file, path,filetype, options).then((request) => request(this.axios, this.basePath));
     }
     /**
      * 

+ 7 - 0
Web/src/api-services/models/file-input.ts

@@ -35,6 +35,13 @@ export interface FileInput {
      * @memberof FileInput
      */
     fileName?: string | null;
+    /**
+     * 文件类型
+     *
+     * @type {string}
+     * @memberof FileInput
+     */
+    fileType?: string | null;
 
     /**
      * 文件Url

+ 7 - 0
Web/src/views/system/file/component/editSysfile.vue

@@ -15,6 +15,13 @@
 						</el-form-item>
 					</el-col>
 				</el-row>
+				<el-row>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
+						<el-form-item label="文件类型" prop="fileType" :rules="[{ required: true, message: '文件类型不能为空', trigger: 'blur' }]">
+							<el-input v-model="state.ruleForm.fileType" placeholder="文件名称" clearable />
+						</el-form-item>
+					</el-col>
+				</el-row>
 			</el-form>
 			<template #footer>
 				<span class="dialog-footer">

+ 12 - 1
Web/src/views/system/file/index.vue

@@ -26,6 +26,10 @@
 		<el-card class="full-table" shadow="hover" style="margin-top: 5px">
 			<el-table :data="state.fileData" style="width: 100%" v-loading="state.loading" border>
 				<el-table-column type="index" label="序号" width="55" align="center" />
+				<el-table-column prop="fileType" label="文件类型" min-width="150" header-align="center" show-overflow-tooltip />
+				<el-table-column type="relationName" label="关联对象名称" align="center" />
+				<el-table-column type="relationtId" label="关联对象ID" align="center" />
+				<el-table-column type="belongId" label="所属ID" align="center" /> 
 				<el-table-column prop="fileName" label="名称" min-width="150" header-align="center" show-overflow-tooltip />
 				<el-table-column prop="suffix" label="后缀" align="center" show-overflow-tooltip>
 					<template #default="scope">
@@ -87,6 +91,12 @@
 				</div>
 			</template>
 			<div>
+				<div> 
+					<el-select v-model="state.fileType" placeholder="请选择文件类型">
+						 <el-option label="相关文件" value="相关文件" />
+						  <el-option label="归档文件" value="归档文件" />
+						 </el-select>
+				 </div>
 				<el-upload ref="uploadRef" drag :auto-upload="false" :limit="1" :file-list="state.fileList" action="" :on-change="handleChange" accept=".jpg,.png,.bmp,.gif,.txt,.pdf,.xlsx,.docx">
 					<el-icon class="el-icon--upload">
 						<ele-UploadFilled />
@@ -163,6 +173,7 @@ const state = reactive({
 	excelUrl: '',
 	pdfUrl: '',
 	fileName: '',
+	fileType:'',
 	previewList: [] as string[],
 });
 
@@ -205,7 +216,7 @@ const handleChange = (file: any, fileList: []) => {
 // 上传
 const uploadFile = async () => {
 	if (state.fileList.length < 1) return;
-	await getAPI(SysFileApi).apiSysFileUploadFilePostForm(state.fileList[0].raw);
+	await getAPI(SysFileApi).apiSysFileUploadFilePostForm(state.fileList[0].raw,undefined,state.fileType);
 	handleQuery();
 	ElMessage.success('上传成功');
 	state.dialogUploadVisible = false;