使用 TypeScript 和 ASP.NET Core 处理文件上传和受保护的下载
使用 Axios、TypeScript、C# 和 ASP.NET Core 处理文件
引言
我最近在开发一个需要上传和下载文件的网站。 似乎这对于某些人来说是一个痛点,所以我想写一篇小文章(因为自从我上一篇文章以来已经很久了,早就应该写了),这对我自己的后代来说意义重大。
背景
我正在构建的应用程序是一个用于销售数字产品的商店。 前端是用 VueJS 编写的,ASP.NET Core 后端 API 为文件和 SPA 提供服务。
使用代码
代码非常容易理解,因此我将只包含每个块正在做什么的简要概述,而不是详细介绍所有内容并混淆主题。
注册您的 API
main.ts
导入您的 api 模块
import api from './services/api'; -- this is my sites api
...
Vue.prototype.$api = api;
api-plugin.d.ts
将 $api
变量附加到 Vue 并为其提供 Api
类型。
import { Api } from './services/api';
declare module 'vue/types/vue' {
interface Vue {
$api: Api;
}
}
export default Api;
上传文件
在我的 VueJS 组件中,我在 data()
对象中创建了一个变量,用于保存要发送到服务器的文件。
files: new FormData(),
我添加了一个处理程序方法来响应用户将文件添加到已上传的文件中
handleFileUpload(fileList: any) {
this.files.append('file', fileList[0], fileList[0].name);
},
Vue 组件模板包含文件输入元素
<input type="file" v-on:change="handleFileUpload($event.target.files)" />
提交文件
当用户在您的 UI 上执行一个触发上传的操作时,我调用我的 API。
this.$api.uploadFile(this.files)
.then((response: <<YourResponseType>>) => {
this.hasError = false;
this.message = 'File uploaded';
}).catch((error) => {
this.hasError = true;
this.message = 'Error uploading file';
});
API 服务方法
上面显示的组件方法轮流调用我的 API 服务上的此方法。
public async uploadFile(fileData: FormData): Promise<<<YourResponseType>>> {
return await axios.post('/api/to/your/upload/handler', fileData, { headers: { 'Content-Type': 'multipart/form-data' } })
.then((response: any) => {
return response.data;
})
.catch((error: any) => {
throw new Error(error);
});
}
ASP.NET Core API 方法
此方法中的代码将根据您自己的要求而有很大差异,但基本结构将类似于此。
[HttpPost("/api/to/your/upload/handler")]
[Consumes("multipart/form-data")]
public async Task<IActionResult> UploadHandler(IFormCollection uploads)
{
if (uploads.Files.Length <= 0) { return BadRequest("No file data"); }
foreach (var f in uploads.Files)
{
var filePath = "YourGeneratedUniqueFilePath";
using (var stream = System.IO.File.Create(filePath))
{
await file.CopyToAsync(stream);
}
}
return Ok();
}
下载文件
这次从服务器端开始,您的 API 方法将类似于这样。 由于我使用的是 Kestral,因此我选择使用 ControllerBase.PhysicalFile()
方法,但您的控制器上也有基本的控制器 ControllerBase.File()
返回方法,以满足您的需求。
由于我的上传与数据存储中的一个实体相关联,因此通过 ID 值请求下载,但您可以使用任何适合您需求的方法。
[HttpGet("[action]")]
public async Task<IActionResult> GetFile(string id)
{
var dir = "GetOrBuildYourDirectoryString";
var fileName = "GetYourFileName";
var mimeType = GetMimeType(fileName);
var path = Path.Combine(dir, fileName);
return PhysicalFile(path, mimeType, version.FileName);
}
public string GetMimeType(string fileName)
{
var provider = new FileExtensionContentTypeProvider();
string contentType;
if (!provider.TryGetContentType(fileName, out contentType))
{
contentType = "application/octet-stream";
}
return contentType;
}
FileExtensionContentTypeProvider
类型来自 Microsoft.AspNetCore.StaticFiles NuGet 包Install-Package Microsoft.AspNetCore.StaticFiles -Version 2.2.0
客户端 API 下载
为了在服务器上调用此 GetFile()
方法,我们的客户端 API 服务需要公开一个下载方法。 这是事情可能会变得有点棘手的地方。 您可能需要配置您的服务器以提供和/或公开内容处置标头。 这超出了本文的范围,因为我想保持主题的简洁性。
我不需要执行任何特定步骤来访问此标头,但我确实不得不执行一些小技巧来提取我在客户端需要的数据 - 主要是文件名。 遗憾的是,此代码并不好。 如果有人对如何改进这一点有建议,请告诉我。
public async downloadFile(id: string): Promise<void> {
return await axios.get('/api/download/getfile?id=' + id, { responseType : 'blob' } )
.then((response: any) => {
const disposition = response.headers['content-disposition'];
let fileName = '';
if (disposition && disposition.indexOf('attachment') !== -1) {
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
const matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
fileName = matches[1].replace(/['"]/g, '');
}
}
const fileUrl = window.URL.createObjectURL(new Blob([response.data]));
const fileLink = document.createElement('a');
fileLink.href = fileUrl;
fileLink.setAttribute('download', fileName);
document.body.appendChild(fileLink);
fileLink.click();
})
.catch((error: any) => {
throw new Error(error);
});
}
这将设置客户端下载并打开本地用户的正常浏览器文件保存对话框。
保护上传的文件
鉴于上面的代码是“手动”处理文件下载和上传,因此浏览器 HTML 中的简单文件 URL 并不理想。 在我的例子中,上传的文件被放置在一个我想处理下载的目录中。
为了保护此“下载”目录,我在 StartUp.cs Configure
方法中将一小段逻辑映射到 .NET Core IApplicationBuilder
实例。 这会拦截对此 URL 的任何请求并发送 401
响应。
app.Map("/downloads", subApp => {
subApp.Use(async (context, next) =>
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
});
});
任何试图访问此 downloads
目录中的文件的用户基本上都会被踢出,并且浏览器会收到服务器的错误响应。
我希望您在此对这种方法简短的描述中找到一些有用的东西。 欢迎任何反馈、建议或改进。
感谢阅读。
历史
V1.0 - 2020 年 3 月 7 日