服务端不暂支持 .net framework ,支持最新 .netcore 添加七牛接口的支持,客户端修改了已知的bug 并对layui支持

This commit is contained in:
Jackson.Bruce
2020-03-30 17:14:07 +08:00
parent b94a9d4032
commit 3a43684ea0
139 changed files with 37319 additions and 9004 deletions

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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();
}
}

View 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; }
}
}

View 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);
}
}

View 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));
}
}

View 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}";
}
}
}

View 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 };
}
}
}

View 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;
}
}
}

View 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>

View 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; }
}
}

View 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;
}
}
}

View 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);
}
}

View 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; }
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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]);
}
}
}
}

View File

@@ -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];
}
}
}

View 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];
}
}
}

View File

@@ -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];
}
}
}

View 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);
}
}
}

View 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; }
}
}

View 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();
}
}
}

View 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
}
}

View 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; }
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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; }
}
}

View 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
}
}