初始化
This commit is contained in:
commit
fbd1edb961
|
|
@ -0,0 +1,5 @@
|
|||
# 2010
|
||||
*.txt -crlf
|
||||
|
||||
# 2020
|
||||
*.txt text eol=lf
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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<FormValueProviderFactory>();
|
||||
factories.RemoveType<FormFileValueProviderFactory>();
|
||||
factories.RemoveType<JQueryFormValueProviderFactory>();
|
||||
}
|
||||
|
||||
public void OnResourceExecuted(ResourceExecutedContext context)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FileController> _logger;
|
||||
|
||||
public FileController(ILogger<FileController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("upload")]
|
||||
[RequestFormLimits(ValueLengthLimit = int.MaxValue, MultipartBodyLengthLimit = long.MaxValue)]
|
||||
[RequestSizeLimit(long.MaxValue)]
|
||||
public async Task<UploadResult> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文件分片上传
|
||||
/// </summary>
|
||||
/// <param name="chunk"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("sliceUpload")]
|
||||
[DisableFormValueModelBinding]
|
||||
public async Task<UploadResult> 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<string> 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<FileSort>();
|
||||
|
||||
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<IOException>()
|
||||
// .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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<StreamingController> _logger;
|
||||
|
||||
public StreamingController(ILogger<StreamingController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action for upload large file
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Request to this action will not trigger any model binding or model validation,
|
||||
/// because this is a no-argument action
|
||||
/// </remarks>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
[DisableFormValueModelBinding]
|
||||
[Route(nameof(UploadLargeFile))]
|
||||
[DisableRequestSizeLimit]
|
||||
[RequestSizeLimit(1_074_790_400)]
|
||||
public async Task<IActionResult> 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>217c497c-0e2e-49f7-85b3-7c69198ee92f</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DG.Core" Version="1.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.8" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.15.1" />
|
||||
<PackageReference Include="Polly" Version="7.2.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="DG.Core" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\files\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
namespace DG.FileServer.WebApi.Models
|
||||
{
|
||||
public class FileChunk
|
||||
{
|
||||
//文件名
|
||||
public string? FileName { get; set; }
|
||||
/// <summary>
|
||||
/// 当前分片
|
||||
/// </summary>
|
||||
public int PartNumber { get; set; }
|
||||
|
||||
private long size;
|
||||
/// <summary>
|
||||
/// 缓冲区大小
|
||||
/// </summary>
|
||||
public long Size
|
||||
{
|
||||
get { return size; }
|
||||
|
||||
set
|
||||
{
|
||||
if (size > 8)
|
||||
{
|
||||
size = 8;
|
||||
}
|
||||
size = value;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 分片总数
|
||||
/// </summary>
|
||||
public long Chunks { get; set; }
|
||||
/// <summary>
|
||||
/// 文件读取起始位置
|
||||
/// </summary>
|
||||
public long Start { get; set; }
|
||||
/// <summary>
|
||||
/// 文件读取结束位置
|
||||
/// </summary>
|
||||
public long End { get; set; }
|
||||
/// <summary>
|
||||
/// 文件大小
|
||||
/// </summary>
|
||||
public long Total { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
namespace DG.FileServer.WebApi.Models
|
||||
{
|
||||
public class FileSort
|
||||
{
|
||||
public const string PART_NUMBER = ".partNumber-";
|
||||
/// <summary>
|
||||
/// 文件名
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
/// <summary>
|
||||
/// 文件分片号
|
||||
/// </summary>
|
||||
public int PartNumber { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 文件读取结束位置
|
||||
/// </summary>
|
||||
public bool IsEnd { get; set; }
|
||||
|
||||
public bool Success { get; set; }
|
||||
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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授权Token:Bearer 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<JwtService>();
|
||||
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();
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
Loading…
Reference in New Issue