服务端不暂支持 .net framework ,支持最新 .netcore 添加七牛接口的支持,客户端修改了已知的bug 并对layui支持
This commit is contained in:
12
FileService/Abstractions/IFileHandler.cs
Normal file
12
FileService/Abstractions/IFileHandler.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ufangx.FileServices.Abstractions
|
||||
{
|
||||
public interface IFileHandler
|
||||
{
|
||||
Task<object> Handler(string path,string fileType,long fileSize);
|
||||
}
|
||||
}
|
||||
86
FileService/Abstractions/IFileService.cs
Normal file
86
FileService/Abstractions/IFileService.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ufangx.FileServices.Abstractions
|
||||
{
|
||||
public interface IFileService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取文件流
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
Task<Stream> GetStream(string path, CancellationToken token = default(CancellationToken));
|
||||
/// <summary>
|
||||
/// 获取文件数据
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
Task<byte[]> GetFileData(string path, CancellationToken token = default(CancellationToken));
|
||||
/// <summary>
|
||||
/// 获取文件更新日期
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<DateTime> GetModifyDate(string path, CancellationToken token = default(CancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// 删除文件
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> Delete(string path, CancellationToken token = default(CancellationToken));
|
||||
/// <summary>
|
||||
/// 保存文件
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="stream"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> Save(string path, Stream stream, CancellationToken token = default(CancellationToken));
|
||||
/// <summary>
|
||||
/// 保存文件
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> Save(string path, byte[] data, CancellationToken token = default(CancellationToken));
|
||||
/// <summary>
|
||||
/// 保存文件,如果文件已经存在将追加数据
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> Append(string path, Stream stream, CancellationToken token = default(CancellationToken));
|
||||
/// <summary>
|
||||
/// 保存文件,如果文件已经存在将追加数据
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> Append(string path, byte[] data, CancellationToken token = default(CancellationToken));
|
||||
|
||||
/// <summary>
|
||||
/// 文件是否存在
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> Exists(string path, CancellationToken token = default(CancellationToken));
|
||||
/// <summary>
|
||||
/// 移动文件
|
||||
/// </summary>
|
||||
/// <param name="sourceFileName"></param>
|
||||
/// <param name="destFileName"></param>
|
||||
/// <returns></returns>
|
||||
Task Move(string sourceFileName, string destFileName);
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
17
FileService/Abstractions/IFileServiceBuilder.cs
Normal file
17
FileService/Abstractions/IFileServiceBuilder.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Ufangx.FileServices.Models;
|
||||
|
||||
namespace Ufangx.FileServices.Abstractions
|
||||
{
|
||||
public interface IFileServiceBuilder
|
||||
{
|
||||
IServiceCollection Services { get; }
|
||||
IFileServiceBuilder AddScheme(string name, Action<FileServiceScheme> configureBuilder);
|
||||
IFileServiceBuilder AddScheme<THandler>(string name, string storeDirectory = null, IEnumerable<string> supportExtensions = null, long? LimitedSize = null) where THandler :class, IFileHandler;
|
||||
IFileServiceBuilder AddAuthenticationScheme(string scheme);
|
||||
IFileServiceBuilder AddAuthenticationSchemes(IEnumerable<string> schemes);
|
||||
}
|
||||
}
|
||||
33
FileService/Abstractions/IFileServiceProvider.cs
Normal file
33
FileService/Abstractions/IFileServiceProvider.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Ufangx.FileServices.Models;
|
||||
|
||||
namespace Ufangx.FileServices.Abstractions
|
||||
{
|
||||
public interface IFileServiceProvider
|
||||
{
|
||||
|
||||
IEnumerable<string> AuthenticationSchemes { get; }
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
string DefaultSchemeName { get; }
|
||||
/// <summary>
|
||||
/// 生成文件名称
|
||||
/// </summary>
|
||||
/// <param name="originName"></param>
|
||||
/// <param name="directory"></param>
|
||||
/// <returns></returns>
|
||||
Task<string> GenerateFileName(string originName, string schemeName, string directory = null);
|
||||
FileValidateResult Validate(string schemeName, string fileName,long fileSize);
|
||||
IFileHandler GetHandler(string schemeName);
|
||||
string GetStoreDirectory(string schemeName);
|
||||
IFileService GetFileService();
|
||||
IResumableService GetResumableService();
|
||||
IEnumerable<FileServiceScheme> GetSchemes();
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
47
FileService/Abstractions/IResumableInfo.cs
Normal file
47
FileService/Abstractions/IResumableInfo.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
|
||||
namespace Ufangx.FileServices.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// 续传信息
|
||||
/// </summary>
|
||||
public interface IResumableInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据块总数
|
||||
/// </summary>
|
||||
long BlobCount { get; }
|
||||
/// <summary>
|
||||
/// 已经处理的数据块总数
|
||||
/// </summary>
|
||||
long BlobIndex { get; set; }
|
||||
/// <summary>
|
||||
/// 数据块的大小
|
||||
/// </summary>
|
||||
long BlobSize { get; }
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
DateTime CreateDate { get; }
|
||||
/// <summary>
|
||||
/// 文件名称
|
||||
/// </summary>
|
||||
string FileName { get; }
|
||||
/// <summary>
|
||||
/// 文件大小
|
||||
/// </summary>
|
||||
long FileSize { get; }
|
||||
/// <summary>
|
||||
/// 文件类型
|
||||
/// </summary>
|
||||
string FileType { get; }
|
||||
/// <summary>
|
||||
/// 唯一键
|
||||
/// </summary>
|
||||
string Key { get; }
|
||||
/// <summary>
|
||||
/// 存储文件名
|
||||
/// </summary>
|
||||
string StoreName { get; }
|
||||
}
|
||||
}
|
||||
16
FileService/Abstractions/IResumableInfoService.cs
Normal file
16
FileService/Abstractions/IResumableInfoService.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ufangx.FileServices.Abstractions
|
||||
{
|
||||
public interface IResumableInfoService
|
||||
{
|
||||
Task<IResumableInfo> Create(string storeName, string fileName, long fileSize, string fileType, long blobCount, int blobSize);
|
||||
Task<IResumableInfo> Get(string key);
|
||||
Task<bool> Update(IResumableInfo resumable);
|
||||
Task<bool> Delete(IResumableInfo resumable);
|
||||
|
||||
}
|
||||
}
|
||||
16
FileService/Abstractions/IResumableService.cs
Normal file
16
FileService/Abstractions/IResumableService.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ufangx.FileServices.Models;
|
||||
|
||||
namespace Ufangx.FileServices.Abstractions
|
||||
{
|
||||
public interface IResumableService: IFileService
|
||||
{
|
||||
Task<bool> SaveBlob(Blob blob, Func<IResumableInfo,bool, Task> finished =null, CancellationToken token = default(CancellationToken));
|
||||
Task<bool> DeleteBlobs(string key, CancellationToken token = default(CancellationToken));
|
||||
|
||||
}
|
||||
}
|
||||
109
FileService/Caching/CacheResumableInfoService.cs
Normal file
109
FileService/Caching/CacheResumableInfoService.cs
Normal file
@@ -0,0 +1,109 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
|
||||
namespace Ufangx.FileServices.Caching
|
||||
{
|
||||
public class CacheResumableInfoService<TResumableInfo> : IResumableInfoService where TResumableInfo : IResumableInfo
|
||||
{
|
||||
protected readonly IDistributedCache cache;
|
||||
protected readonly ILogger<CacheResumableInfoService<TResumableInfo>> logger;
|
||||
protected readonly IHttpContextAccessor contextAccessor;
|
||||
|
||||
public CacheResumableInfoService(IDistributedCache cache,ILogger<CacheResumableInfoService<TResumableInfo>> logger, IHttpContextAccessor contextAccessor)
|
||||
{
|
||||
this.cache = cache;
|
||||
this.logger = logger;
|
||||
this.contextAccessor = contextAccessor;
|
||||
}
|
||||
|
||||
public virtual async Task<IResumableInfo> Create(string storeName, string fileName, long fileSize, string fileType, long blobCount, int blobSize)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(storeName))
|
||||
{
|
||||
throw new ArgumentException("message", nameof(storeName));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
throw new ArgumentException("message", nameof(fileName));
|
||||
}
|
||||
|
||||
string cleanKey = $"StoreName={storeName}&FileName={fileName}&FileType={fileType}&FileSize={fileSize}&BlobSize={blobSize}&BlobCount={blobCount}&user={contextAccessor?.HttpContext?.User?.Identity?.Name}";
|
||||
byte[] data;
|
||||
using (var md5 = MD5.Create())
|
||||
{
|
||||
data = md5.ComputeHash(Encoding.UTF8.GetBytes(cleanKey));
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
sb.Append(data[i].ToString("X2"));
|
||||
}
|
||||
string key = sb.ToString();
|
||||
|
||||
var info = await Get(key);
|
||||
if (info != null) return info;
|
||||
|
||||
var json = JsonConvert.SerializeObject(new { StoreName = storeName, Key = key, FileType = fileType, FileSize = fileSize, FileName = fileName, CreateDate = DateTime.Now, BlobSize = blobSize, BlobIndex = 0, BlobCount = blobCount });
|
||||
|
||||
return await Save(key, json) ? JsonConvert.DeserializeObject<TResumableInfo>(json) : default(TResumableInfo);
|
||||
}
|
||||
|
||||
public virtual async Task<bool> Delete(IResumableInfo resumable)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cachekey = GetCacheKey(resumable.Key);
|
||||
await cache.RemoveAsync(cachekey);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
logger.LogError(ex, "移除缓存失败");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public virtual async Task<IResumableInfo> Get(string key)
|
||||
{
|
||||
var data = await cache.GetStringAsync(GetCacheKey(key));
|
||||
if (data == null) return default(TResumableInfo);
|
||||
var info = JsonConvert.DeserializeObject<TResumableInfo>(data);
|
||||
return info;
|
||||
}
|
||||
protected virtual async Task<bool> Save(string key, object info) {
|
||||
try
|
||||
{
|
||||
string data;
|
||||
if (info is string json)
|
||||
{
|
||||
data = json;
|
||||
}
|
||||
else {
|
||||
data = JsonConvert.SerializeObject(info);
|
||||
}
|
||||
await cache.SetStringAsync(GetCacheKey(key), data);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "更新续传信息失败");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public async Task<bool> Update(IResumableInfo resumable)
|
||||
=> await Save(resumable.Key, resumable);
|
||||
|
||||
protected virtual string GetCacheKey(string key) {
|
||||
return $"{GetType().FullName}&key={key}&user={contextAccessor?.HttpContext?.User?.Identity?.Name}";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
17
FileService/FileServiceBuilderServiceCollectionExtensions.cs
Normal file
17
FileService/FileServiceBuilderServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Ufangx.FileServices;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
using Ufangx.FileServices.Models;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
public static class FileServiceBuilderServiceCollectionExtensions
|
||||
{
|
||||
public static IFileServiceBuilder AddFileServiceBuilder(this IServiceCollection services, Action<FileServiceOptions> configureBuilder = null) {
|
||||
services.AddTransient<IFileServiceProvider, FileServiceProvider>();
|
||||
return new FileServiceBuilder() { Services = services };
|
||||
}
|
||||
}
|
||||
}
|
||||
139
FileService/FileServiceProvider.cs
Normal file
139
FileService/FileServiceProvider.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
using Ufangx.FileServices.Models;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Ufangx.FileServices
|
||||
{
|
||||
public class FileServiceProvider : IFileServiceProvider,IDisposable
|
||||
{
|
||||
private readonly FileServiceOptions options;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly IServiceScope serviceScope;
|
||||
|
||||
public FileServiceProvider(IOptions<FileServiceOptions> options, IServiceProvider serviceProvider)
|
||||
{
|
||||
this.options = options.Value;
|
||||
this.serviceScope = serviceProvider.CreateScope();
|
||||
this.serviceProvider = serviceScope.ServiceProvider;
|
||||
}
|
||||
|
||||
public IEnumerable<string> AuthenticationSchemes => options.AuthenticationSchemes;
|
||||
|
||||
public string DefaultSchemeName => options.DefaultTopic;
|
||||
|
||||
FileServiceScheme GetScheme(string name) {
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("message", nameof(name));
|
||||
}
|
||||
var topic = options.SchemeMap[name];
|
||||
if (topic == null)
|
||||
{
|
||||
throw new Exception($"无效的文件服务方案名称“{name}”");
|
||||
}
|
||||
return topic;
|
||||
}
|
||||
public IFileHandler GetHandler(string schemeName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(schemeName)) return null;
|
||||
var scheme = GetScheme(schemeName);
|
||||
if (scheme.HandlerType == null) return null;
|
||||
if (!typeof(IFileHandler).IsAssignableFrom(scheme.HandlerType)) {
|
||||
throw new Exception($"类型“{scheme.HandlerType.FullName}”没有实现“{typeof(IFileHandler).FullName}”接口");
|
||||
}
|
||||
return serviceProvider.GetService(scheme.HandlerType) as IFileHandler;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public string GetStoreDirectory(string schemeName)
|
||||
=> string.IsNullOrWhiteSpace(schemeName) ? string.Empty : GetScheme(schemeName).StoreDirectory;
|
||||
|
||||
public async Task<string> GenerateFileName(string originName, string schemeName, string directory = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(originName))
|
||||
{
|
||||
throw new ArgumentException("message", nameof(originName));
|
||||
}
|
||||
|
||||
FileNameRule nameRule = options?.RuleOptions?.Rule ?? FileNameRule.Ascending;
|
||||
if (nameRule == FileNameRule.Custom && options?.RuleOptions?.Custom==null) {
|
||||
nameRule = FileNameRule.Ascending;
|
||||
}
|
||||
if (directory == null) { directory = string.Empty; }
|
||||
directory = Path.Combine(GetStoreDirectory(schemeName), directory);
|
||||
string fileName;
|
||||
switch (nameRule)
|
||||
{
|
||||
case FileNameRule.Ascending:
|
||||
fileName = Path.Combine(directory, originName);
|
||||
int index = 0;
|
||||
var fileService = GetFileService();
|
||||
while (await fileService.Exists(fileName))
|
||||
{
|
||||
fileName = Path.Combine(directory, $"{Path.GetFileNameWithoutExtension(originName)}({++index}){Path.GetExtension(originName)}");
|
||||
}
|
||||
break;
|
||||
case FileNameRule.Date:
|
||||
fileName = Path.Combine(directory, string.Format(options?.RuleOptions?.Format ?? "{0:yyyyMMddHHmmss}", DateTime.Now) + Path.GetExtension(originName));
|
||||
break;
|
||||
case FileNameRule.Custom:
|
||||
fileName = Path.Combine(directory, options.RuleOptions.Custom(originName));
|
||||
break;
|
||||
default:
|
||||
fileName = Path.Combine(directory, originName);
|
||||
break;
|
||||
}
|
||||
return fileName.Replace('\\', '/');
|
||||
|
||||
}
|
||||
|
||||
public FileValidateResult Validate(string schemeName, string fileName,long fileSize)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(schemeName)) {
|
||||
return FileValidateResult.Successfully;
|
||||
}
|
||||
var scheme = GetScheme(schemeName);
|
||||
if (scheme.LimitedSize.HasValue && scheme.LimitedSize.Value < fileSize) {
|
||||
return FileValidateResult.Limited;
|
||||
}
|
||||
|
||||
string ext = Path.GetExtension(fileName);
|
||||
return
|
||||
scheme.SupportExtensions==null
|
||||
|| scheme.SupportExtensions.Count()==0
|
||||
|| scheme.SupportExtensions.Any(e => string.Equals(ext, e, StringComparison.OrdinalIgnoreCase))
|
||||
? FileValidateResult.Successfully : FileValidateResult.Invalid;
|
||||
|
||||
|
||||
}
|
||||
|
||||
public IFileService GetFileService()
|
||||
{
|
||||
return serviceProvider.GetService<IFileService>();
|
||||
}
|
||||
|
||||
public IResumableService GetResumableService()
|
||||
{
|
||||
return serviceProvider.GetService<IResumableService>();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
serviceScope.Dispose();
|
||||
}
|
||||
|
||||
public IEnumerable<FileServiceScheme> GetSchemes()
|
||||
{
|
||||
return options.Schemes;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
FileService/FileServices.csproj
Normal file
34
FileService/FileServices.csproj
Normal file
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netcoreapp2.0;netstandard2.1;netcoreapp3.0</TargetFrameworks>
|
||||
<RootNamespace>Ufangx.FileServices</RootNamespace>
|
||||
<AssemblyName>FileServices</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)|$(Platform)'=='netstandard2.0|AnyCPU'">
|
||||
<DefineConstants>TRACE;netstandard20</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="3.1.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.0'">
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.Abstractions" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Localization.Abstractions" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
11
FileService/Local/LocalFileOption.cs
Normal file
11
FileService/Local/LocalFileOption.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ufangx.FileServices
|
||||
{
|
||||
public class LocalFileOption
|
||||
{
|
||||
public string StorageRootDir { get; set; }
|
||||
}
|
||||
}
|
||||
164
FileService/Local/LocalFileService.cs
Normal file
164
FileService/Local/LocalFileService.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
|
||||
namespace Ufangx.FileServices.Local
|
||||
{
|
||||
public class LocalFileService : IFileService
|
||||
{
|
||||
private readonly LocalFileOption option;
|
||||
|
||||
public LocalFileService(IOptions<LocalFileOption> option) {
|
||||
|
||||
this.option = option.Value; // option??new LocalFileOption();
|
||||
if (string.IsNullOrWhiteSpace(this.option.StorageRootDir)) {
|
||||
this.option.StorageRootDir = AppContext.BaseDirectory;
|
||||
}
|
||||
}
|
||||
protected string physicalPath(string path) {
|
||||
|
||||
return Path.Combine(option.StorageRootDir, path.Trim().Replace('/', '\\').TrimStart('\\'));
|
||||
}
|
||||
protected bool CreateDirIfNonexistence(string path) {
|
||||
|
||||
string dir = Path.GetDirectoryName(path);
|
||||
if (!Directory.Exists(dir)) {
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
public async Task<bool> Delete(string path, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
string p = physicalPath(path);
|
||||
if (File.Exists(p))
|
||||
{
|
||||
File.Delete(p);
|
||||
}
|
||||
return await Task.FromResult(true);
|
||||
|
||||
}
|
||||
|
||||
public async Task<bool> Exists(string path, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
return await Task.FromResult(File.Exists(physicalPath(path)));
|
||||
}
|
||||
|
||||
public async Task<Stream> GetStream(string path, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
var p = physicalPath(path);
|
||||
if (!File.Exists(p)) return null;
|
||||
return await Task.FromResult(new FileStream(p, FileMode.Open, FileAccess.Read,FileShare.ReadWrite| FileShare.Delete));
|
||||
|
||||
}
|
||||
|
||||
public async Task<byte[]> GetFileData(string path, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
var p = physicalPath(path);
|
||||
if (!File.Exists(p)) return null;
|
||||
#if netstandard20
|
||||
return await Task.FromResult(File.ReadAllBytes(p));
|
||||
#else
|
||||
return await File.ReadAllBytesAsync(p, token);
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
public async Task<bool> Save(string path, Stream stream, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
var p = physicalPath(path);
|
||||
if (CreateDirIfNonexistence(p))
|
||||
{
|
||||
if (stream.CanSeek && stream.Position > 0) { stream.Position = 0; }
|
||||
using (var fs = new FileStream(p, FileMode.Create))
|
||||
{
|
||||
int len = 10485760;
|
||||
byte[] buffer = new byte[len];
|
||||
int readed;
|
||||
while ((readed = await stream.ReadAsync(buffer, 0, len, token)) > 0)
|
||||
{
|
||||
await fs.WriteAsync(buffer, 0, Math.Min(readed, len), token);
|
||||
await fs.FlushAsync(token);
|
||||
}
|
||||
fs.Close();
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> Save(string path, byte[] data, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
var p = physicalPath(path);
|
||||
if (CreateDirIfNonexistence(p))
|
||||
{
|
||||
#if netstandard20
|
||||
File.WriteAllBytes(p, data);
|
||||
await Task.Yield();
|
||||
#else
|
||||
await File.WriteAllBytesAsync(p, data, token);
|
||||
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public async Task Move(string sourceFileName,string destFileName) {
|
||||
sourceFileName = physicalPath(sourceFileName);
|
||||
destFileName = physicalPath(destFileName);
|
||||
File.Move(sourceFileName, destFileName);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
public async Task<DateTime> GetModifyDate(string path, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
return await Task.FromResult(File.GetLastWriteTime(physicalPath(path)));
|
||||
}
|
||||
|
||||
public async Task<bool> Append(string path, Stream stream, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
var p = physicalPath(path);
|
||||
if (CreateDirIfNonexistence(p))
|
||||
{
|
||||
if (stream.CanSeek && stream.Position > 0) { stream.Position = 0; }
|
||||
using (var fs = new FileStream(p, FileMode.Append,FileAccess.Write,FileShare.Read))
|
||||
{
|
||||
int len = 10485760;
|
||||
byte[] buffer = new byte[len];
|
||||
int readed;
|
||||
while ((readed = await stream.ReadAsync(buffer, 0, len, token)) > 0)
|
||||
{
|
||||
await fs.WriteAsync(buffer, 0, Math.Min(readed, len), token);
|
||||
await fs.FlushAsync(token);
|
||||
}
|
||||
fs.Close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> Append(string path, byte[] data, CancellationToken token = default(CancellationToken))
|
||||
{
|
||||
var p = physicalPath(path);
|
||||
if (CreateDirIfNonexistence(p))
|
||||
{
|
||||
using (var fs = new FileStream(p, FileMode.Append, FileAccess.Write, FileShare.Read))
|
||||
{
|
||||
await fs.WriteAsync(data, 0, data.Length, token);
|
||||
await fs.FlushAsync(token);
|
||||
fs.Close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
FileService/Local/LocalFileSystemExtensions.cs
Normal file
25
FileService/Local/LocalFileSystemExtensions.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Ufangx.FileServices;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
using Ufangx.FileServices.Caching;
|
||||
using Ufangx.FileServices.Local;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
public static class LocalFileSystemExtensions
|
||||
{
|
||||
public static IFileServiceBuilder AddLocalServices<TResumableInfoService>(this IFileServiceBuilder builder,Action<LocalFileOption> action)where TResumableInfoService: class,IResumableInfoService
|
||||
{
|
||||
var services = builder.Services;
|
||||
services.AddTransient<IFileService, LocalFileService>();
|
||||
services.AddTransient<IResumableInfoService, TResumableInfoService>();
|
||||
services.AddTransient<IResumableService, LocalResumableService>();
|
||||
services.Configure(action);
|
||||
return builder;
|
||||
}
|
||||
public static IFileServiceBuilder AddLocalServices(this IFileServiceBuilder builder, Action<LocalFileOption> action)
|
||||
=> AddLocalServices<CacheResumableInfoService<LocalResumableInfo>>(builder, action);
|
||||
}
|
||||
}
|
||||
48
FileService/Local/LocalResumableInfo.cs
Normal file
48
FileService/Local/LocalResumableInfo.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
|
||||
namespace Ufangx.FileServices.Local
|
||||
{
|
||||
public class LocalResumableInfo : IResumableInfo
|
||||
{
|
||||
public string Key { get; set; }
|
||||
/// <summary>
|
||||
/// 文件名称
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
/// <summary>
|
||||
/// 文件类型
|
||||
/// </summary>
|
||||
public string FileType { get; set; }
|
||||
/// <summary>
|
||||
/// 存储位置
|
||||
/// </summary>
|
||||
public string StoreName { get; set; }
|
||||
/// <summary>
|
||||
/// 文件大小
|
||||
/// </summary>
|
||||
public long FileSize { get; set; }
|
||||
/// <summary>
|
||||
/// 切片大小
|
||||
/// </summary>
|
||||
public long BlobSize { get; set; }
|
||||
/// <summary>
|
||||
/// 切片总数
|
||||
/// </summary>
|
||||
public long BlobCount { get; set; }
|
||||
/// <summary>
|
||||
/// 已经完成的切片索引
|
||||
/// </summary>
|
||||
public long BlobIndex { get; set; }
|
||||
/// <summary>
|
||||
/// 创建的时间
|
||||
/// </summary>
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
160
FileService/Local/LocalResumableService.cs
Normal file
160
FileService/Local/LocalResumableService.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
using Ufangx.FileServices.Models;
|
||||
|
||||
namespace Ufangx.FileServices.Local
|
||||
{
|
||||
public class LocalResumableService : LocalFileService, IResumableService
|
||||
{
|
||||
private readonly IResumableInfoService resumableInfoService;
|
||||
public LocalResumableService(IResumableInfoService resumableInfoService, IOptions<LocalFileOption> option):base(option) {
|
||||
this.resumableInfoService = resumableInfoService;
|
||||
}
|
||||
FileStream GetFileStream(string path) {
|
||||
return new FileStream(path, FileMode.Create, FileAccess.Write);
|
||||
}
|
||||
string GetTempDir(string path, string key)
|
||||
{
|
||||
return Path.Combine(Path.GetDirectoryName(path), $"_tmp{key}");
|
||||
}
|
||||
bool CheckFiles(string dir, long count) {
|
||||
//Console.WriteLine("正在检查文件。。。");
|
||||
//Stopwatch sw = Stopwatch.StartNew();
|
||||
for (long i = 0; i < count; i++)
|
||||
{
|
||||
if (!File.Exists(Path.Combine(dir, $"{i}"))) { return false; }
|
||||
}
|
||||
//sw.Stop();
|
||||
//Console.WriteLine($"检查{count}个文件,用时:{sw.Elapsed.TotalMilliseconds}毫秒");
|
||||
return true;
|
||||
}
|
||||
async Task<byte[]> GetBlob(string fileName, CancellationToken token = default)
|
||||
{
|
||||
#if netstandard20
|
||||
return await Task.FromResult(File.ReadAllBytes(fileName));
|
||||
#else
|
||||
return await File.ReadAllBytesAsync(fileName, token);
|
||||
#endif
|
||||
}
|
||||
async void MakeFile(string dir,string path, long count, CancellationToken token = default) {
|
||||
try
|
||||
{
|
||||
//Console.WriteLine($"开始创建“{path}”文件。。。。");
|
||||
//Stopwatch sw = Stopwatch.StartNew();
|
||||
using (var fs = GetFileStream(path))
|
||||
{
|
||||
for (long i = 0; i < count; i++)
|
||||
{
|
||||
var blob = await GetBlob(Path.Combine(dir, $"{i}"), token);
|
||||
await fs.WriteAsync(blob, 0, blob.Length, token);
|
||||
}
|
||||
await fs.FlushAsync(token);
|
||||
fs.Close();
|
||||
}
|
||||
//Directory.Delete(dir, true);
|
||||
//sw.Stop();
|
||||
//Console.WriteLine($"用时:{sw.Elapsed.TotalMilliseconds}毫秒,创建“{path}”文件。");
|
||||
}
|
||||
catch(Exception ex) {
|
||||
if (File.Exists(path)) { File.Delete(path); }
|
||||
//Console.WriteLine("error:" + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Directory.Delete(dir, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
public async Task<bool> SaveBlob(Blob blob, Func<IResumableInfo,bool, Task> finished = null, CancellationToken token = default)
|
||||
{
|
||||
if (blob is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(blob));
|
||||
}
|
||||
|
||||
var info = await resumableInfoService.Get(blob.ResumableKey);
|
||||
if (info == null) {
|
||||
throw new Exception($"无效的{nameof(blob.ResumableKey)}");
|
||||
}
|
||||
var p = physicalPath(info.StoreName);
|
||||
string tempdir = GetTempDir(p,info.Key);
|
||||
var tmp = Path.Combine(tempdir, $"{blob.BlobIndex}");
|
||||
if (CreateDirIfNonexistence(tmp))
|
||||
{
|
||||
var stream = blob.Data;
|
||||
using (var fs = GetFileStream(tmp))
|
||||
{
|
||||
int len = 10485760;
|
||||
byte[] buffer = new byte[len];
|
||||
int readed;
|
||||
while ((readed = await stream.ReadAsync(buffer, 0, len, token)) > 0)
|
||||
{
|
||||
await fs.WriteAsync(buffer, 0, Math.Min(readed, len), token);
|
||||
await fs.FlushAsync(token);
|
||||
}
|
||||
fs.Close();
|
||||
}
|
||||
if (blob.BlobIndex>= info.BlobCount-1)
|
||||
{
|
||||
bool ok = false;
|
||||
if (CheckFiles(tempdir, info.BlobCount)) {
|
||||
|
||||
MakeFile(tempdir, p, info.BlobCount);
|
||||
//Console.WriteLine("后台线程创建文件。。。");
|
||||
ok = true;
|
||||
}
|
||||
if (finished != null) { await finished(info,ok); }
|
||||
await resumableInfoService.Delete(info);
|
||||
return ok;
|
||||
}
|
||||
else {
|
||||
info.BlobIndex++;
|
||||
await resumableInfoService.Update(info);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteBlobs(string key, CancellationToken token = default)
|
||||
{
|
||||
var info = await resumableInfoService.Get(key);
|
||||
if (info == null)
|
||||
{
|
||||
throw new Exception($"无效的{nameof(key)}");
|
||||
}
|
||||
|
||||
if (await resumableInfoService.Delete(info)) {
|
||||
string tempdir = GetTempDir(physicalPath(info.StoreName), info.Key);
|
||||
try
|
||||
{
|
||||
Directory.Delete(tempdir, true);
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
//删除文件失败写回续传信息
|
||||
await resumableInfoService.Update(info);
|
||||
throw;
|
||||
}
|
||||
|
||||
}
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
40
FileService/Middlewares/FileServiceAppBuilderExtensions.cs
Normal file
40
FileService/Middlewares/FileServiceAppBuilderExtensions.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Ufangx.FileServices.Middlewares;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
public static class FileServiceAppBuilderExtensions
|
||||
{
|
||||
internal static PathString PathBase { get; private set; }
|
||||
public static IApplicationBuilder UseFileServices(this IApplicationBuilder app)
|
||||
{
|
||||
return UseFileServices(app, new PathString());
|
||||
}
|
||||
public static IApplicationBuilder UseFileServices(this IApplicationBuilder app, PathString pathMatch)
|
||||
{
|
||||
|
||||
if (!pathMatch.HasValue)
|
||||
{
|
||||
pathMatch = "/FileServices";
|
||||
}
|
||||
PathBase = pathMatch;
|
||||
app.Map(pathMatch, pathRoute => {
|
||||
pathRoute.Map("/uploader", route =>
|
||||
{
|
||||
route.MapWhen(ctx => string.Equals("GET", ctx.Request.Method, StringComparison.OrdinalIgnoreCase), a => a.UseMiddleware<ResumableInfoUploaderMiddleware>())
|
||||
.MapWhen(ctx => string.Equals("DELETE", ctx.Request.Method, StringComparison.OrdinalIgnoreCase), a => a.UseMiddleware<ResumableInfoDeleteUploaderMiddleware>())
|
||||
.MapWhen(ctx =>
|
||||
string.Equals("POST", ctx.Request.Method, StringComparison.OrdinalIgnoreCase) && ctx.Request.Form.Files.Count== 1 && !string.IsNullOrWhiteSpace(ctx.Request.Form["key"]) && !string.IsNullOrWhiteSpace(ctx.Request.Form["blobIndex"]),
|
||||
a => a.UseMiddleware<ResumableServiceUploaderMiddleware>())
|
||||
.MapWhen(ctx => string.Equals("POST", ctx.Request.Method, StringComparison.OrdinalIgnoreCase) && ctx.Request.Form.Files.Count > 0,
|
||||
a => a.UseMiddleware<FileServiceUploaderMiddleware>());
|
||||
});
|
||||
|
||||
});
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
FileService/Middlewares/FileServiceUploaderMiddleware.cs
Normal file
75
FileService/Middlewares/FileServiceUploaderMiddleware.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace Ufangx.FileServices.Middlewares
|
||||
{
|
||||
public class FileServiceUploaderMiddleware: UploaderMiddleware
|
||||
{
|
||||
private IFileService service;
|
||||
|
||||
public IFileService Service
|
||||
=> service??(service=serviceProvider.GetFileService());
|
||||
|
||||
public FileServiceUploaderMiddleware(RequestDelegate next,
|
||||
IFileServiceProvider serviceProvider) : base(next, serviceProvider)
|
||||
{
|
||||
}
|
||||
protected override string GetRequestParams(string key)
|
||||
{
|
||||
return Context.Request.Form[key];
|
||||
}
|
||||
async Task SingleHandler(IFormFile file) {
|
||||
if (await ValidateResultHandler(Validate(file.FileName, file.Length)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
string path = await GetStoreFileName(file.FileName);
|
||||
if (await Service.Save(path, file.OpenReadStream()))
|
||||
{
|
||||
await WriteJsonAsync(await SchemeHandler(path, file.ContentType, file.Length));
|
||||
return;
|
||||
}
|
||||
await Error(StringLocalizer["保存文件失败"]);
|
||||
|
||||
}
|
||||
async Task MultiHandler(IFormFileCollection files) {
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (await ValidateResultHandler(Validate(file.FileName, file.Length)))
|
||||
{
|
||||
//如果有文件验证失败则返回
|
||||
return;
|
||||
}
|
||||
}
|
||||
List<object> results = new List<object>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
string path = await GetStoreFileName(file.FileName);
|
||||
if (await Service.Save(path, file.OpenReadStream()))
|
||||
{
|
||||
results.Add(await SchemeHandler(path, file.ContentType, file.Length));
|
||||
}
|
||||
}
|
||||
await WriteJsonAsync(results);
|
||||
}
|
||||
protected async override Task Handler(HttpContext context) {
|
||||
if (context.Request.Form.Files.Count > 1)
|
||||
{
|
||||
await MultiHandler(context.Request.Form.Files);
|
||||
|
||||
}
|
||||
else if(context.Request.Form.Files.Count==1) {
|
||||
await SingleHandler(context.Request.Form.Files[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace Ufangx.FileServices.Middlewares
|
||||
{
|
||||
public class ResumableInfoDeleteUploaderMiddleware : UploaderMiddleware
|
||||
{
|
||||
private IResumableService service;
|
||||
|
||||
public IResumableService Service => service??(service=serviceProvider.GetResumableService());
|
||||
public ResumableInfoDeleteUploaderMiddleware(RequestDelegate next,
|
||||
IFileServiceProvider serviceProvider) : base(next, serviceProvider)
|
||||
{
|
||||
|
||||
}
|
||||
protected override async Task Handler(HttpContext context) {
|
||||
await WriteJsonAsync(await Service.DeleteBlobs(GetRequestParams("key")));
|
||||
}
|
||||
protected override string GetRequestParams(string key)
|
||||
{
|
||||
return Context.Request.Form[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
66
FileService/Middlewares/ResumableInfoUploaderMiddleware .cs
Normal file
66
FileService/Middlewares/ResumableInfoUploaderMiddleware .cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace Ufangx.FileServices.Middlewares
|
||||
{
|
||||
public class ResumableInfoUploaderMiddleware : UploaderMiddleware
|
||||
{
|
||||
private IResumableInfoService service;
|
||||
|
||||
public IResumableInfoService Service => service ?? (service = Context.RequestServices.GetRequiredService<IResumableInfoService>());
|
||||
|
||||
public ResumableInfoUploaderMiddleware(RequestDelegate next,
|
||||
IFileServiceProvider serviceProvider) : base(next, serviceProvider)
|
||||
{
|
||||
}
|
||||
protected override async Task Handler(HttpContext context) {
|
||||
string key = GetRequestParams("key");
|
||||
IResumableInfo info = null;
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
if (long.TryParse(GetRequestParams("fileSize"), out long fileSize)
|
||||
&& int.TryParse(GetRequestParams("blobSize"), out int blobSize)
|
||||
&& long.TryParse(GetRequestParams("blobCount"), out long blobCount))
|
||||
{
|
||||
string fileName = GetRequestParams("fileName");
|
||||
if (string.IsNullOrWhiteSpace(fileName)) {
|
||||
await Error(StringLocalizer["参数文件名称(fileName)是必须的"]);
|
||||
return;
|
||||
}
|
||||
if (await ValidateResultHandler(Validate(fileName, fileSize))) {
|
||||
return;
|
||||
}
|
||||
info = await Service.Create(await GetStoreFileName(fileName),
|
||||
fileName,
|
||||
fileSize,
|
||||
GetRequestParams("fileType"),
|
||||
blobCount,
|
||||
blobSize);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
info = await Service.Get(key);
|
||||
}
|
||||
await WriteJsonAsync(info == null ? null : new
|
||||
{
|
||||
key = info.Key,
|
||||
index = info.BlobIndex,
|
||||
});
|
||||
}
|
||||
|
||||
protected override string GetRequestParams(string key)
|
||||
{
|
||||
return Context.Request.Query[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Ufangx.FileServices.Models;
|
||||
using Microsoft.Extensions.Localization;
|
||||
|
||||
namespace Ufangx.FileServices.Middlewares
|
||||
{
|
||||
public class ResumableServiceUploaderMiddleware : UploaderMiddleware
|
||||
{
|
||||
private IResumableService _service;
|
||||
private readonly ILogger<ResumableServiceUploaderMiddleware> logger;
|
||||
|
||||
public IResumableService Service => _service ?? (_service = serviceProvider.GetResumableService());
|
||||
|
||||
public ResumableServiceUploaderMiddleware(RequestDelegate next,
|
||||
ILogger<ResumableServiceUploaderMiddleware> logger,
|
||||
IFileServiceProvider serviceProvider) : base(next, serviceProvider)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
protected override async Task Handler(HttpContext context) {
|
||||
var blobIndex = long.Parse(GetRequestParams("blobIndex"));
|
||||
string resumableKey = GetRequestParams("key");
|
||||
bool finished = false;
|
||||
await Service.SaveBlob(new Blob() { BlobIndex = blobIndex, ResumableKey = resumableKey, Data = context.Request.Form.Files[0].OpenReadStream() },
|
||||
async (info,success) =>
|
||||
{
|
||||
finished = true;
|
||||
if (success)
|
||||
{
|
||||
await WriteJsonAsync(await SchemeHandler(info.StoreName, info.FileType, info.FileSize));
|
||||
}
|
||||
else {
|
||||
await Error(StringLocalizer["保存文件失败"]);
|
||||
}
|
||||
});
|
||||
if (!finished)
|
||||
{
|
||||
await WriteJsonAsync(true);
|
||||
}
|
||||
}
|
||||
protected override string GetRequestParams(string key)
|
||||
{
|
||||
return Context.Request.Form[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
126
FileService/Middlewares/UploaderMiddleware.cs
Normal file
126
FileService/Middlewares/UploaderMiddleware.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using System.Linq;
|
||||
using Ufangx.FileServices.Models;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Ufangx.FileServices.Middlewares
|
||||
{
|
||||
public abstract class UploaderMiddleware
|
||||
{
|
||||
protected readonly RequestDelegate next;
|
||||
protected readonly IFileServiceProvider serviceProvider;
|
||||
|
||||
|
||||
public UploaderMiddleware(RequestDelegate next,IFileServiceProvider serviceProvider)
|
||||
{
|
||||
this.next = next;
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
protected HttpContext Context { get; private set; }
|
||||
private IStringLocalizer stringLocalizer;
|
||||
protected IStringLocalizer StringLocalizer
|
||||
=> stringLocalizer??(stringLocalizer=Context.RequestServices.GetService<IStringLocalizer>()??new DefaultStringLocalizer());
|
||||
|
||||
|
||||
protected abstract string GetRequestParams(string key);
|
||||
protected async Task<string> GetStoreFileName(string originName) {
|
||||
|
||||
string dir = GetRequestParams("dir");
|
||||
string fileName = GetRequestParams("Name");
|
||||
string scheme = GetScheme();
|
||||
//如果客户端指定了
|
||||
if (!string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return Path.Combine(serviceProvider.GetStoreDirectory(scheme), dir, fileName).Replace('\\', '/');
|
||||
}
|
||||
//否则生成文件名称
|
||||
return await serviceProvider.GenerateFileName(originName, scheme, dir);
|
||||
|
||||
}
|
||||
async Task<bool> Authenticate(HttpContext context) {
|
||||
if (serviceProvider.AuthenticationSchemes?.Count() > 0) {
|
||||
foreach ( var scheme in serviceProvider.AuthenticationSchemes)
|
||||
{
|
||||
var result = await context.AuthenticateAsync(scheme);
|
||||
if (result.Succeeded) {
|
||||
context.User = result.Principal;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return context.User.Identity.IsAuthenticated;
|
||||
//return true;
|
||||
}
|
||||
string _scheme;
|
||||
protected string GetScheme() {
|
||||
if (_scheme == null)
|
||||
{
|
||||
_scheme =Context.Request.Headers["scheme"];
|
||||
if (string.IsNullOrWhiteSpace(_scheme))
|
||||
{
|
||||
_scheme = serviceProvider.DefaultSchemeName ?? string.Empty;
|
||||
}
|
||||
}
|
||||
return _scheme;
|
||||
}
|
||||
protected async Task<object> SchemeHandler(string path,string fileType,long fileSize) {
|
||||
var handler = serviceProvider.GetHandler(GetScheme());
|
||||
if (handler == null) return path;
|
||||
return await handler.Handler(path, fileType, fileSize);
|
||||
|
||||
}
|
||||
protected FileValidateResult Validate(string fileName,long fileSize) {
|
||||
return serviceProvider.Validate(GetScheme(), fileName, fileSize);
|
||||
}
|
||||
protected abstract Task Handler(HttpContext context);
|
||||
protected async Task<bool> ValidateResultHandler(FileValidateResult fileValidateResult)
|
||||
{
|
||||
if (fileValidateResult == FileValidateResult.Successfully) return false;
|
||||
await Error(StringLocalizer[fileValidateResult == FileValidateResult.Invalid ? "不支持的文件类型" : "文件大小超过了最大限制"]);
|
||||
return true;
|
||||
}
|
||||
protected Task Error(object error, int code = 500) {
|
||||
Context.Response.StatusCode = code;
|
||||
return WriteJsonAsync(error);
|
||||
}
|
||||
protected Task Error(string message, int code = 500)
|
||||
=> Error((object)message, code);
|
||||
protected Task WriteAsync(string content) => WriteJsonAsync(content);
|
||||
protected Task WriteJsonAsync(object obj)
|
||||
=> Context.Response.WriteAsync(JsonConvert.SerializeObject(obj,
|
||||
Formatting.Indented,
|
||||
new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
}), Encoding.UTF8);
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
Context = context;
|
||||
context.Response.ContentType = "application/json; charset=UTF-8";
|
||||
if (await Authenticate(context)) {
|
||||
await Handler(context);
|
||||
return;
|
||||
}
|
||||
context.Response.StatusCode = 401;
|
||||
await WriteJsonAsync(StringLocalizer["身份认证失败!"].Value);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
14
FileService/Models/Blob.cs
Normal file
14
FileService/Models/Blob.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Ufangx.FileServices.Models
|
||||
{
|
||||
public class Blob
|
||||
{
|
||||
public Stream Data { get; set; }
|
||||
public string ResumableKey { get; set; }
|
||||
public long BlobIndex { get; set; }
|
||||
}
|
||||
}
|
||||
25
FileService/Models/DefaultStringLocalizer.cs
Normal file
25
FileService/Models/DefaultStringLocalizer.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Microsoft.Extensions.Localization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Ufangx.FileServices.Models
|
||||
{
|
||||
internal class DefaultStringLocalizer : IStringLocalizer
|
||||
{
|
||||
LocalizedString IStringLocalizer.this[string name] => new LocalizedString(name, name,false);
|
||||
|
||||
LocalizedString IStringLocalizer.this[string name, params object[] arguments] =>new LocalizedString(name, string.Format(name, arguments),false) ;
|
||||
|
||||
IEnumerable<LocalizedString> IStringLocalizer.GetAllStrings(bool includeParentCultures)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
IStringLocalizer IStringLocalizer.WithCulture(CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
29
FileService/Models/FileNameRule.cs
Normal file
29
FileService/Models/FileNameRule.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ufangx.FileServices.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件命名规则
|
||||
/// </summary>
|
||||
public enum FileNameRule
|
||||
{
|
||||
/// <summary>
|
||||
/// 保持原名,如果存在同名则在原名后递增数量
|
||||
/// </summary>
|
||||
Ascending,
|
||||
/// <summary>
|
||||
/// 保持原名,如果存在同名则覆盖
|
||||
/// </summary>
|
||||
Keep,
|
||||
/// <summary>
|
||||
/// 用日期命名
|
||||
/// </summary>
|
||||
Date,
|
||||
/// <summary>
|
||||
/// 自定义
|
||||
/// </summary>
|
||||
Custom
|
||||
}
|
||||
}
|
||||
14
FileService/Models/FileNameRuleOptions.cs
Normal file
14
FileService/Models/FileNameRuleOptions.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ufangx.FileServices.Models
|
||||
{
|
||||
public class FileNameRuleOptions
|
||||
{
|
||||
public FileNameRule Rule { get; set; }
|
||||
public string Format { get; set; }
|
||||
public Func<string, string> Custom { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
49
FileService/Models/FileServiceBuilder.cs
Normal file
49
FileService/Models/FileServiceBuilder.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
using Ufangx.FileServices.Models;
|
||||
|
||||
namespace Ufangx.FileServices.Models
|
||||
{
|
||||
public class FileServiceBuilder : IFileServiceBuilder
|
||||
{
|
||||
public IServiceCollection Services { get; set; }
|
||||
|
||||
public IFileServiceBuilder AddAuthenticationScheme(string scheme)
|
||||
{
|
||||
Services.Configure<FileServiceOptions>(opt => opt.AddAuthenticationScheme(scheme));
|
||||
return this;
|
||||
}
|
||||
|
||||
public IFileServiceBuilder AddAuthenticationSchemes(IEnumerable<string> schemes)
|
||||
{
|
||||
Services.Configure<FileServiceOptions>(opt => opt.AddAuthenticationSchemes(schemes));
|
||||
return this;
|
||||
}
|
||||
|
||||
public IFileServiceBuilder AddScheme(string name, Action<FileServiceScheme> configureBuilder)
|
||||
{
|
||||
Services.Configure<FileServiceOptions>(opt => opt.AddScheme(name, configureBuilder));
|
||||
return this;
|
||||
}
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <typeparam name="THandler"></typeparam>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="storeDirectory"></param>
|
||||
/// <param name="supportExtensions"></param>
|
||||
/// <param name="LimitedSize"></param>
|
||||
/// <returns></returns>
|
||||
public IFileServiceBuilder AddScheme<THandler>(string name, string storeDirectory = null, IEnumerable<string> supportExtensions = null, long? LimitedSize = null)
|
||||
where THandler :class, IFileHandler
|
||||
{
|
||||
|
||||
Services.Configure<FileServiceOptions>(opt => opt.AddScheme<THandler>(name, storeDirectory, supportExtensions, LimitedSize));
|
||||
Services.AddTransient<THandler>();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
94
FileService/Models/FileServiceOptions.cs
Normal file
94
FileService/Models/FileServiceOptions.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
|
||||
namespace Ufangx.FileServices.Models
|
||||
{
|
||||
public class FileServiceOptions
|
||||
{
|
||||
private readonly IList<FileServiceScheme> _schemes = new List<FileServiceScheme>();
|
||||
private readonly IList<string> _authenticationSchemes = new List<string>();
|
||||
public string DefaultTopic { get; set; }
|
||||
public IEnumerable<FileServiceScheme> Schemes => _schemes;
|
||||
public IEnumerable<string> AuthenticationSchemes => _authenticationSchemes;
|
||||
public IDictionary<string, FileServiceScheme> SchemeMap { get; } = new Dictionary<string, FileServiceScheme>(StringComparer.Ordinal);
|
||||
/// <summary>
|
||||
/// 命名规则设置
|
||||
/// </summary>
|
||||
public FileNameRuleOptions RuleOptions { get; set; } = new FileNameRuleOptions() { Rule = FileNameRule.Ascending };
|
||||
/// <summary>
|
||||
/// 添加文件服务方案
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="configureBuilder"></param>
|
||||
/// <returns></returns>
|
||||
public FileServiceOptions AddScheme(string name, Action<FileServiceScheme> configureBuilder)
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
if (configureBuilder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configureBuilder));
|
||||
}
|
||||
if (SchemeMap.ContainsKey(name))
|
||||
{
|
||||
throw new InvalidOperationException("方案名称已经存在:" + name);
|
||||
}
|
||||
var scheme = new FileServiceScheme(name);
|
||||
configureBuilder(scheme);
|
||||
_schemes.Add(scheme);
|
||||
SchemeMap[name] = scheme;
|
||||
return this;
|
||||
}
|
||||
/// <summary>
|
||||
/// 添加文件服务方案
|
||||
/// </summary>
|
||||
/// <typeparam name="THandler">方案处理程序类型</typeparam>
|
||||
/// <param name="name">方案名称</param>
|
||||
/// <param name="storeDirectory">文件存储目录</param>
|
||||
/// <param name="supportExtensions">支持的扩展名称</param>
|
||||
/// <returns></returns>
|
||||
public FileServiceOptions AddScheme<THandler>(string name, string storeDirectory = null, IEnumerable<string> supportExtensions = null, long? LimitedSize = null)
|
||||
where THandler : class, IFileHandler
|
||||
=> AddScheme(name, opt =>
|
||||
{
|
||||
opt.HandlerType = typeof(THandler);
|
||||
opt.StoreDirectory = storeDirectory;
|
||||
if (supportExtensions != null)
|
||||
{
|
||||
opt.SupportExtensions = supportExtensions;
|
||||
}
|
||||
if (LimitedSize != null)
|
||||
{
|
||||
opt.LimitedSize = LimitedSize;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 添加认证方案
|
||||
/// </summary>
|
||||
/// <param name="scheme"></param>
|
||||
/// <returns></returns>
|
||||
public FileServiceOptions AddAuthenticationScheme(string scheme) {
|
||||
if (_authenticationSchemes.Contains(scheme)) {
|
||||
throw new Exception($"认证方案已经存在:{scheme}");
|
||||
}
|
||||
_authenticationSchemes.Add(scheme);
|
||||
return this;
|
||||
}
|
||||
/// <summary>
|
||||
/// 添加认证方案
|
||||
/// </summary>
|
||||
/// <param name="schemes"></param>
|
||||
/// <returns></returns>
|
||||
public FileServiceOptions AddAuthenticationSchemes(IEnumerable<string> schemes)
|
||||
{
|
||||
foreach (var scheme in schemes) AddAuthenticationScheme(scheme);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
FileService/Models/FileServiceScheme.cs
Normal file
40
FileService/Models/FileServiceScheme.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Ufangx.FileServices.Abstractions;
|
||||
|
||||
namespace Ufangx.FileServices.Models
|
||||
{
|
||||
public class FileServiceScheme
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件服务方案
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
public FileServiceScheme(string name) {
|
||||
Name = name;
|
||||
}
|
||||
/// <summary>
|
||||
/// 方案名称
|
||||
/// </summary>
|
||||
public string Name { get;}
|
||||
/// <summary>
|
||||
/// 存储目录
|
||||
/// </summary>
|
||||
public string StoreDirectory { get; set; }
|
||||
/// <summary>
|
||||
/// 支持的文件扩展名
|
||||
/// </summary>
|
||||
public IEnumerable<string> SupportExtensions { get; set; } = Enumerable.Empty<string>();
|
||||
/// <summary>
|
||||
/// 文件大小的最大限制
|
||||
/// </summary>
|
||||
public long? LimitedSize { get; set; }
|
||||
/// <summary>
|
||||
/// 处理类型
|
||||
/// </summary>
|
||||
public Type HandlerType { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
22
FileService/Models/FileValidateResult.cs
Normal file
22
FileService/Models/FileValidateResult.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Ufangx.FileServices.Models
|
||||
{
|
||||
public enum FileValidateResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证成功
|
||||
/// </summary>
|
||||
Successfully,
|
||||
/// <summary>
|
||||
/// 文件大小受限制
|
||||
/// </summary>
|
||||
Limited,
|
||||
/// <summary>
|
||||
/// 文件类型是无效的
|
||||
/// </summary>
|
||||
Invalid
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user