commit fbd1edb96143790c20c6bb5a95d42c942b2ff0e6 Author: zhuxiaojiong <645680426@qq.com> Date: Sat Jun 28 11:03:48 2025 +0800 初始化 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..908fb28 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# 2010 +*.txt -crlf + +# 2020 +*.txt text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e112cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,258 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates +*.DS_Store +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +logs/ +[Bb]in/ +[Oo]bj/ +results/ + +# Visual Studio 2015 cache/options directory +.vs/ +.vscode/ +# Uncomment if you have tasks that create the project's static files in wwwroot +wwwroot/ +site/wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +!idsrv3test.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ +!tools/packages.config +tools/ + +# MacOS +.DS_Store + +# Ocelot acceptance test config +test/Ocelot.AcceptanceTests/ocelot.json + +# Read the docstates +_build/ +_static/ +_templates/ + +# JetBrains Rider +.idea/ + +# Test Results +*.trx diff --git a/DG.FileServer/.dockerignore b/DG.FileServer/.dockerignore new file mode 100644 index 0000000..3729ff0 --- /dev/null +++ b/DG.FileServer/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/DG.FileServer/DG.FileServer.WebApi/Attributes/DisableFormValueModelBindingAttribute.cs b/DG.FileServer/DG.FileServer.WebApi/Attributes/DisableFormValueModelBindingAttribute.cs new file mode 100644 index 0000000..1fec46c --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/Attributes/DisableFormValueModelBindingAttribute.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using System; + +namespace DG.FileServer.WebApi.Attributes +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public sealed class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter + { + public void OnResourceExecuting(ResourceExecutingContext context) + { + var factories = context.ValueProviderFactories; + factories.RemoveType(); + factories.RemoveType(); + factories.RemoveType(); + } + + public void OnResourceExecuted(ResourceExecutedContext context) + { + } + } +} diff --git a/DG.FileServer/DG.FileServer.WebApi/Controllers/FileController.cs b/DG.FileServer/DG.FileServer.WebApi/Controllers/FileController.cs new file mode 100644 index 0000000..05ec8c2 --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/Controllers/FileController.cs @@ -0,0 +1,274 @@ +using DG.FileServer.WebApi.Attributes; +using DG.FileServer.WebApi.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Net.Http.Headers; +using Polly; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Authorization; +using System.Net; + +namespace DG.FileServer.WebApi.Controllers +{ + [Authorize] + [ApiController] + [Route("[controller]")] + public class FileController : ControllerBase + { + private const string DEFAULT_FOLDER = "//wwwroot//files//"; + private readonly ILogger _logger; + + public FileController(ILogger logger) + { + _logger = logger; + } + + [HttpPost("upload")] + [RequestFormLimits(ValueLengthLimit = int.MaxValue, MultipartBodyLengthLimit = long.MaxValue)] + [RequestSizeLimit(long.MaxValue)] + public async Task ActionCreateAsync(IFormFile file) + { + //var file = Request.Body.Files[0]; + var fileName = file.FileName; + var path = Environment.CurrentDirectory + @"//wwwroot//files//"; + var suffix = fileName.Split('.').Last(); + + if (!Directory.Exists(path + suffix)) + { + Directory.CreateDirectory(path + suffix); + } + + var fileFullName = Environment.CurrentDirectory + @"//wwwroot//files//" + suffix + "//" + fileName; + await using var sourceStream = new MemoryStream(); + await file.CopyToAsync(sourceStream); + var result = new UploadResult(false, false, ""); + try + { + //· + string savePath = Path.GetDirectoryName(fileFullName); + if (!Directory.Exists(savePath)) + { + Directory.CreateDirectory(savePath); + } + + using FileStream fsTarget = new(fileFullName, FileMode.Create, FileAccess.Write, FileShare.None); + fsTarget.Write(sourceStream.ToArray(), 0, sourceStream.ToArray().Length); + + + fsTarget.Flush(); + fsTarget.Close(); + var uri = new Uri($"{Request.Scheme}://{Request.Host.Value}{fileFullName.Replace("app//", "").Replace("wwwroot/", "StaticFiles").Replace(suffix + "/", suffix).Replace("files/", "files")}"); + result.Success = true; + result.IsEnd = true; + result.Url = uri.AbsoluteUri; + } + catch(Exception ex) + { + _logger.LogError(ex, ""); + return result; + } + return result; + } + + /// + /// ļƬϴ + /// + /// + /// + [HttpPost("sliceUpload")] + [DisableFormValueModelBinding] + public async Task SliceUpload([FromQuery] FileChunk chunk) + { + var result = new UploadResult(false, false, ""); + try + { + if (!IsMultipartContentType(Request.ContentType)) + { + return result; + } + + var boundary = GetBoundary(); + if (string.IsNullOrEmpty(boundary)) + { + return result; + } + + var reader = new MultipartReader(boundary, Request.Body); + + var section = await reader.ReadNextSectionAsync(); + + while (section != null) + { + var buffer = new byte[1024]; + var fileName = GetFileName(section.ContentDisposition); + chunk.FileName = fileName; + var path = Path.Combine(Environment.CurrentDirectory + DEFAULT_FOLDER, fileName); + using (var stream = new FileStream(path, FileMode.Append)) + { + int bytesRead; + do + { + while ((bytesRead = await section.Body.ReadAsync(buffer)) > 0) + { + await stream.WriteAsync(buffer.AsMemory(0, bytesRead)); + } + + } while (bytesRead > 0); + } + + section = await reader.ReadNextSectionAsync(); + } + + //ϴļСʵʱȣTODO) + + //ϲļ漰תȣ + if (chunk.PartNumber == chunk.Chunks) + { + var url = await MergeChunkFile(chunk); + if (!string.IsNullOrEmpty(url)) + { + var uri = new Uri($"{Request.Scheme}://{Request.Host.Value}{url.Replace("app//", "").Replace("wwwroot/", "StaticFiles").Replace("files/", "files")}"); + result.Success = true; + result.IsEnd = true; + result.Url = uri.AbsoluteUri; + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, ""); + return result; + } + result.Success = true; + return result; + } + + private bool IsMultipartContentType(string contentType) + { + return + !string.IsNullOrEmpty(contentType) && + contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0; + } + + private string GetBoundary() + { + var mediaTypeHeaderContentType = MediaTypeHeaderValue.Parse(Request.ContentType); + + return HeaderUtilities.RemoveQuotes(mediaTypeHeaderContentType.Boundary).Value; + } + + private string GetFileName(string contentDisposition) + { + return contentDisposition + .Split(';') + .SingleOrDefault(part => part.Contains("filename")) + .Split('=') + .Last() + .Trim('"'); + } + + private async Task MergeChunkFile(FileChunk chunk) + { + var uploadDirectoryName = Path.Combine(Environment.CurrentDirectory + DEFAULT_FOLDER, chunk.FileName); + + var partToken = FileSort.PART_NUMBER; + + var baseFileName = chunk.FileName.Substring(0, chunk.FileName.IndexOf(partToken)); + + var searchpattern = $"{Path.GetFileName(baseFileName)}{partToken}*"; + + var filesList = Directory.GetFiles(Path.GetDirectoryName(uploadDirectoryName), searchpattern); + if (!filesList.Any()) { return ""; } + + var mergeFiles = new List(); + + foreach (string fileName in filesList) + { + var fileNameNumber = fileName.Substring(fileName.IndexOf(FileSort.PART_NUMBER) + + FileSort.PART_NUMBER.Length); + + int.TryParse(fileNameNumber, out var number); + if (number <= 0) + { + continue; + } + + mergeFiles.Add(new FileSort + { + FileName = fileName, + PartNumber = number + }); + } + + // շƬ + var mergeFileSorts = mergeFiles.OrderBy(s => s.PartNumber).ToArray(); + + // ϲļ + var fileFullPath = Path.Combine(Environment.CurrentDirectory + DEFAULT_FOLDER, baseFileName); + if (System.IO.File.Exists(fileFullPath)) + { + System.IO.File.Delete(fileFullPath); + } + + var maxInfo = new FileInfo(mergeFileSorts.First().FileName).Length; + foreach (FileSort fileSort in mergeFileSorts) + { + var fileInfo = new FileInfo(fileSort.FileName); + if (fileSort == mergeFileSorts.Last()) break; + while (maxInfo > fileInfo.Length) + { + await Task.Delay(200); + fileInfo = new FileInfo(fileSort.FileName); + } + } + + using var fileStream = new FileStream(fileFullPath, FileMode.Create); + + foreach (FileSort fileSort in mergeFileSorts) + { + using FileStream fileChunk = + new(fileSort.FileName, FileMode.Open, + FileAccess.Read, FileShare.Read); + var buffer = fileSort == mergeFileSorts.Last() ? new byte[fileChunk.ReadByte()] : new byte[1024]; + int bytesRead; + do + { + while ((bytesRead = await fileChunk.ReadAsync(buffer)) > 0) + { + await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead)); + } + } while (bytesRead > 0); + } + + //await Policy.Handle() + // .RetryForeverAsync() + // .ExecuteAsync(async () => + // { + // foreach (FileSort fileSort in mergeFileSorts) + // { + // using FileStream fileChunk = + // new(fileSort.FileName, FileMode.Open, + // FileAccess.Read, FileShare.Read); + + // await fileChunk.CopyToAsync(fileStream); + // } + // }); + + + //ɾƬļ + //Parallel.ForEach(mergeFiles, f => + //{ + // System.IO.File.Delete(f.FileName); + //}); + + return fileFullPath; + } + } +} \ No newline at end of file diff --git a/DG.FileServer/DG.FileServer.WebApi/Controllers/StreamingController.cs b/DG.FileServer/DG.FileServer.WebApi/Controllers/StreamingController.cs new file mode 100644 index 0000000..fe0adf2 --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/Controllers/StreamingController.cs @@ -0,0 +1,101 @@ +using DG.FileServer.WebApi.Attributes; +using DG.FileServer.WebApi.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; +using System.IO; +using System.Threading.Tasks; + +namespace DG.FileServer.WebApi.Controllers +{ + [Authorize] + [ApiController] + [Route("[controller]")] + public class StreamingController : Controller + { + private const string DEFAULT_FOLDER = "//wwwroot//files//"; + private readonly ILogger _logger; + + public StreamingController(ILogger logger) + { + _logger = logger; + } + + /// + /// Action for upload large file + /// + /// + /// Request to this action will not trigger any model binding or model validation, + /// because this is a no-argument action + /// + /// + [HttpPost] + [DisableFormValueModelBinding] + [Route(nameof(UploadLargeFile))] + [DisableRequestSizeLimit] + [RequestSizeLimit(1_074_790_400)] + public async Task UploadLargeFile() + { + var request = HttpContext.Request; + + // validation of Content-Type + // 1. first, it must be a form-data request + // 2. a boundary should be found in the Content-Type + if (!request.HasFormContentType || + !MediaTypeHeaderValue.TryParse(request.ContentType, out var mediaTypeHeader) || + string.IsNullOrEmpty(mediaTypeHeader.Boundary.Value)) + { + return new UnsupportedMediaTypeResult(); + } + + var reader = new MultipartReader(mediaTypeHeader.Boundary.Value, request.Body); + var section = await reader.ReadNextSectionAsync(); + var result = new UploadResult(false, false, ""); + // This sample try to get the first file from request and save it + // Make changes according to your needs in actual use + while (section != null) + { + var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, + out var contentDisposition); + + if (hasContentDispositionHeader && contentDisposition.DispositionType.Equals("form-data") && + !string.IsNullOrEmpty(contentDisposition.FileName.Value)) + { + // Don't trust any file name, file extension, and file data from the request unless you trust them completely + // Otherwise, it is very likely to cause problems such as virus uploading, disk filling, etc + // In short, it is necessary to restrict and verify the upload + // Here, we just use the temporary folder and a random file name + + // Get the temporary folder, and combine a random file name with it + var fileName = contentDisposition.FileName.Value; + + var savePath = Path.Combine(Environment.CurrentDirectory + DEFAULT_FOLDER + Guid.NewGuid().ToString()); + if (!Directory.Exists(savePath))//判断文件夹是否存在 + { + Directory.CreateDirectory(savePath);//不存在则创建文件夹 + } + var saveToPath = Path.Combine(savePath, fileName); + //var saveToPath = Path.Combine(Path.GetTempPath(), fileName); + + using (var targetStream = System.IO.File.Create(saveToPath)) + { + await section.Body.CopyToAsync(targetStream); + } + var uri = new Uri($"{Request.Scheme}://{Request.Host.Value}{saveToPath.Replace("app//", "").Replace("wwwroot/", "StaticFiles").Replace("files/", "files")}"); + result.Success = true; + result.IsEnd = true; + result.Url = uri.AbsoluteUri; + return Ok(result); + } + + section = await reader.ReadNextSectionAsync(); + } + + // If the code runs to this location, it means that no files have been saved + return BadRequest("No files data in the request."); + } + } +} diff --git a/DG.FileServer/DG.FileServer.WebApi/Controllers/TokenController.cs b/DG.FileServer/DG.FileServer.WebApi/Controllers/TokenController.cs new file mode 100644 index 0000000..348c474 --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/Controllers/TokenController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace DG.FileServer.WebApi.Controllers +{ + [Route("api/[controller]")] + [ApiController] + [AllowAnonymous] + public class TokenController : ControllerBase + { + private readonly JwtService _jwtService; + public TokenController(JwtService jwtService) + { + _jwtService = jwtService; + } + + [HttpGet] + public string GetToken(string key) + { + return _jwtService.CreateToken(key); + } + } +} diff --git a/DG.FileServer/DG.FileServer.WebApi/DG.FileServer.WebApi.csproj b/DG.FileServer/DG.FileServer.WebApi/DG.FileServer.WebApi.csproj new file mode 100644 index 0000000..0010586 --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/DG.FileServer.WebApi.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + enable + 217c497c-0e2e-49f7-85b3-7c69198ee92f + Linux + + + + + + + + + + + + + + + + + + + diff --git a/DG.FileServer/DG.FileServer.WebApi/Dockerfile b/DG.FileServer/DG.FileServer.WebApi/Dockerfile new file mode 100644 index 0000000..23de155 --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/Dockerfile @@ -0,0 +1,22 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src +COPY ["DG.FileServer.WebApi/DG.FileServer.WebApi.csproj", "DG.FileServer.WebApi/"] +RUN dotnet restore "DG.FileServer.WebApi/DG.FileServer.WebApi.csproj" +COPY . . +WORKDIR "/src/DG.FileServer.WebApi" +RUN dotnet build "DG.FileServer.WebApi.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "DG.FileServer.WebApi.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "DG.FileServer.WebApi.dll"] \ No newline at end of file diff --git a/DG.FileServer/DG.FileServer.WebApi/JwtService.cs b/DG.FileServer/DG.FileServer.WebApi/JwtService.cs new file mode 100644 index 0000000..b560d26 --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/JwtService.cs @@ -0,0 +1,57 @@ +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace DG.FileServer.WebApi +{ + public class JwtService + { + private readonly IConfiguration _configuration; + + public JwtService(IConfiguration configuration) + { + _configuration = configuration; + } + + public string CreateToken(string key) + { + if (_configuration["Jwt:Key"] != key) + { + return ""; + } + // 1. 定义需要使用到的Claims + var claims = new[] + { + new Claim(ClaimTypes.Name, "admin"), //HttpContext.User.Identity.Name + new Claim(ClaimTypes.Role, "admin"), //HttpContext.User.IsInRole("r_admin") + new Claim(JwtRegisteredClaimNames.Jti, "admin"), + new Claim("keys", _configuration["Jwt:Key"]) + }; + + // 2. 从 appsettings.json 中读取SecretKey + var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"])); + + // 3. 选择加密算法 + var algorithm = SecurityAlgorithms.HmacSha256; + + // 4. 生成Credentials + var signingCredentials = new SigningCredentials(secretKey, algorithm); + + // 5. 根据以上,生成token + var jwtSecurityToken = new JwtSecurityToken( + _configuration["Jwt:Issuer"], //Issuer + _configuration["Jwt:Audience"], //Audience + claims, //Claims, + DateTime.Now, //notBefore + DateTime.Now.AddHours(2), //expires + signingCredentials //Credentials + ); + + // 6. 将token变为string + var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); + + return token; + } + } +} diff --git a/DG.FileServer/DG.FileServer.WebApi/Models/FileChunk.cs b/DG.FileServer/DG.FileServer.WebApi/Models/FileChunk.cs new file mode 100644 index 0000000..3771ccb --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/Models/FileChunk.cs @@ -0,0 +1,46 @@ +namespace DG.FileServer.WebApi.Models +{ + public class FileChunk + { + //文件名 + public string? FileName { get; set; } + /// + /// 当前分片 + /// + public int PartNumber { get; set; } + + private long size; + /// + /// 缓冲区大小 + /// + public long Size + { + get { return size; } + + set + { + if (size > 8) + { + size = 8; + } + size = value; + } + } + /// + /// 分片总数 + /// + public long Chunks { get; set; } + /// + /// 文件读取起始位置 + /// + public long Start { get; set; } + /// + /// 文件读取结束位置 + /// + public long End { get; set; } + /// + /// 文件大小 + /// + public long Total { get; set; } + } +} diff --git a/DG.FileServer/DG.FileServer.WebApi/Models/FileSort.cs b/DG.FileServer/DG.FileServer.WebApi/Models/FileSort.cs new file mode 100644 index 0000000..5f7c0be --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/Models/FileSort.cs @@ -0,0 +1,15 @@ +namespace DG.FileServer.WebApi.Models +{ + public class FileSort + { + public const string PART_NUMBER = ".partNumber-"; + /// + /// 文件名 + /// + public string FileName { get; set; } + /// + /// 文件分片号 + /// + public int PartNumber { get; set; } + } +} diff --git a/DG.FileServer/DG.FileServer.WebApi/Models/UploadResult.cs b/DG.FileServer/DG.FileServer.WebApi/Models/UploadResult.cs new file mode 100644 index 0000000..f946492 --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/Models/UploadResult.cs @@ -0,0 +1,24 @@ +using System; + +namespace DG.FileServer.WebApi.Models +{ + public class UploadResult + { + public UploadResult(bool isEnd, bool success, string? url) + { + IsEnd = isEnd; + Success = success; + Url = url; + } + + + /// + /// ļȡλ + /// + public bool IsEnd { get; set; } + + public bool Success { get; set; } + + public string? Url { get; set; } + } +} diff --git a/DG.FileServer/DG.FileServer.WebApi/MultipartRequestHelper.cs b/DG.FileServer/DG.FileServer.WebApi/MultipartRequestHelper.cs new file mode 100644 index 0000000..98f3d60 --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/MultipartRequestHelper.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using Microsoft.Net.Http.Headers; + +namespace DG.FileServer.WebApi +{ + public static class MultipartRequestHelper + { + // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" + // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit. + public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit) + { + var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; + + if (string.IsNullOrWhiteSpace(boundary)) + { + throw new InvalidDataException("Missing content-type boundary."); + } + + if (boundary.Length > lengthLimit) + { + throw new InvalidDataException( + $"Multipart boundary length limit {lengthLimit} exceeded."); + } + + return boundary; + } + + public static bool IsMultipartContentType(string contentType) + { + return !string.IsNullOrEmpty(contentType) + && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0; + } + + public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition) + { + // Content-Disposition: form-data; name="key"; + return contentDisposition != null + && contentDisposition.DispositionType.Equals("form-data") + && string.IsNullOrEmpty(contentDisposition.FileName.Value) + && string.IsNullOrEmpty(contentDisposition.FileNameStar.Value); + } + + public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition) + { + // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg" + return contentDisposition != null + && contentDisposition.DispositionType.Equals("form-data") + && (!string.IsNullOrEmpty(contentDisposition.FileName.Value) + || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value)); + } + } +} diff --git a/DG.FileServer/DG.FileServer.WebApi/Program.cs b/DG.FileServer/DG.FileServer.WebApi/Program.cs new file mode 100644 index 0000000..6e0e5cd --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/Program.cs @@ -0,0 +1,117 @@ +using DG.Core; +using DG.FileServer.WebApi; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.Extensions.FileProviders; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using System.Text; + +var rootPath = Path.Combine(Environment.CurrentDirectory, "wwwroot"); +if (!Directory.Exists(rootPath)) +{ + Directory.CreateDirectory(rootPath); +} + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +var MyAllowSpecificOrigins = "_myAllowSpecificOrigins"; +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() + { + Description = "¿ͷҪJwtȨTokenBearer Token", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + BearerFormat = "JWT", + Scheme = "Bearer" + }); + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] { } + } + }); +}); +builder.Services.AddHttpContextAccessor(); +//ע +builder.Services.AddAuthentication(options => +{ + options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; +}) +.AddJwtBearer(options => +{ + options.TokenValidationParameters = new TokenValidationParameters() + { + ValidateIssuer = true, //Ƿ֤Issuer + ValidIssuer = builder.Configuration["Jwt:Issuer"], //Issuer + ValidateAudience = true, //Ƿ֤Audience + ValidAudience = builder.Configuration["Jwt:Audience"], //Audience + ValidateIssuerSigningKey = true, //Ƿ֤SecurityKey + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"])), //SecurityKey + ValidateLifetime = true, //Ƿ֤ʧЧʱ + ClockSkew = TimeSpan.FromSeconds(30), //ʱݴֵʱ䲻ͬ⣨룩 + RequireExpirationTime = true, + }; +}); +builder.Services.AddSingleton(); +builder.Services.AddCors(option => +{ + option.AddPolicy(MyAllowSpecificOrigins, + policy => + { + policy.AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod(); + }); +}); +var app = builder.Build(); + + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment() || Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "PreProduction") +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseCors(MyAllowSpecificOrigins); +var provider = new FileExtensionContentTypeProvider(); +provider.Mappings[".7z"] = "application/octet-stream"; +provider.Mappings[".apk"] = "application/octet-stream"; +app.UseStaticFiles(new StaticFileOptions +{ + ContentTypeProvider = provider, + FileProvider = new PhysicalFileProvider( + Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot")), + RequestPath = new PathString("/StaticFiles") +}); +app.UseFileServer(new FileServerOptions +{ + FileProvider = new PhysicalFileProvider( + Path.Combine(builder.Environment.ContentRootPath, "wwwroot")), + RequestPath = "/StaticFiles", + EnableDirectoryBrowsing = true +}); +app.UseHttpsRedirection(); + +//мUseAuthentication֤Ҫ֤мǰã UseAuthorizationȨ +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/DG.FileServer/DG.FileServer.WebApi/Properties/launchSettings.json b/DG.FileServer/DG.FileServer.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..17c109d --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/Properties/launchSettings.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:18294", + "sslPort": 44326 + } + }, + "profiles": { + "DG.FileServer.WebApi": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5300;http://localhost:5299", + "dotnetRunMessages": true + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "publishAllPorts": true, + "useSSL": true, + "httpPort": "5299", + "sslPort": "5300" + } + } +} \ No newline at end of file diff --git a/DG.FileServer/DG.FileServer.WebApi/appsettings.Development.json b/DG.FileServer/DG.FileServer.WebApi/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/DG.FileServer/DG.FileServer.WebApi/appsettings.json b/DG.FileServer/DG.FileServer.WebApi/appsettings.json new file mode 100644 index 0000000..0c89dab --- /dev/null +++ b/DG.FileServer/DG.FileServer.WebApi/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Jwt": { + "SecretKey": "7AC51A5F0DE9A13D5FC9960AD45CC8D5", + "Issuer": "DG.FileServer.WebApi", + "Audience": "DG.FileServer.WebApi", + "Key": "7AC51A5F0DE9A13D5FC9960AD45CC8D5" + } +} diff --git a/DG.FileServer/DG.FileServer.sln b/DG.FileServer/DG.FileServer.sln new file mode 100644 index 0000000..1084021 --- /dev/null +++ b/DG.FileServer/DG.FileServer.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32616.157 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DG.FileServer.WebApi", "DG.FileServer.WebApi\DG.FileServer.WebApi.csproj", "{EA23DEB8-EE24-4FE5-B88C-C10F2C2A47CB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EA23DEB8-EE24-4FE5-B88C-C10F2C2A47CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA23DEB8-EE24-4FE5-B88C-C10F2C2A47CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA23DEB8-EE24-4FE5-B88C-C10F2C2A47CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA23DEB8-EE24-4FE5-B88C-C10F2C2A47CB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {90DCFE54-54D9-475F-80FE-7963E26798A9} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md new file mode 100644 index 0000000..560474e --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# DG.FileServer +