基于AspNetWebApi实现简单的文件服务

基于AspNetWebApi实现简单的文件服务

文章目录

概要

说到文件服务可能很多朋友会选择集成minio这些框架,但是对于一些大部分的系统来说,不会用到很复杂的场景。本文将分享基于AspNet WebApi(.NET4.5)实现一个简单文件服务。

设计思路

1、所有文件放在网站的根目录下Uploads文件夹中(其实也考虑过放其他盘,但是我预览文件要通过URL访问,所以只有放网站目录下);
2、文件名格式为“文件ID.扩展名”,后面访问文件也是通过文件Id查找;
3、设计文件上传、获取文件路径、删除文件三个接口;
4、封装一个工具类,用于调用文件服务。

具体代码实现

1、文件服务API接口

1、单个文件限制最大100M,这个可以自己调整(这里需要注意WebApi项目本身也是对请求的报文有限制,这个怎么弄搜一下就行);
2、对于允许哪些扩展名的文件,配置文件里面配AllowedExtensions

BaseController代码

publicabstractclassBaseController:ApiController{protectedIHttpActionResultSuccess(string message ="",object data =null){returnJson(new{ Code =0, Message = message, Data = data });}protectedIHttpActionResultFail(string message ="",object data =null){returnJson(new{ Code =1, Message = message, Data = data });}}

Web.config

<appSettings><add key="AllowedExtensions"value="jpg,jpeg,png,gif,pdf,txt,doc,docx,xls,xlsx,zip,rar"/></appSettings>

FileController

publicclassFileController:BaseController{privateconststring folder ="~/Uploads";privatereadonlystring uploadFolder;privatereadonlylong maxFileSize =100*1024*1024;privatereadonlystring[] allowedExtensions;publicFileController(){ allowedExtensions = ConfigurationManager.AppSettings["AllowedExtensions"].Split(',').Select(s =>"."+ s).ToArray(); uploadFolder = HttpContext.Current.Server.MapPath(folder);if(!Directory.Exists(uploadFolder)) Directory.CreateDirectory(uploadFolder);}[HttpPost]publicasyncTask<IHttpActionResult>UploadFile(){if(!Request.Content.IsMimeMultipartContent())returnFail("请选择文件上传");var provider =newMultipartFormDataStreamProvider(Path.GetTempPath());await Request.Content.ReadAsMultipartAsync(provider);if(!provider.FileData.Any())returnFail("没有上传文件");var resultFiles =newList<object>();var savedFiles =newList<string>();var baseUrl =$"{Request.RequestUri.Scheme}://{Request.RequestUri.Authority}";var relativePath = folder.TrimStart('~');try{foreach(var fileData in provider.FileData){var tempPath = fileData.LocalFileName;var originalName = fileData.Headers.ContentDisposition.FileName.Trim('\"');var extension = Path.GetExtension(originalName).ToLower();if(!allowedExtensions.Contains(extension))thrownewException($"不支持的文件类型: {extension}");var fileInfo =newFileInfo(tempPath);if(fileInfo.Length > maxFileSize)thrownewException($"文件大小超过100MB: {originalName}");var fileId = Guid.NewGuid().ToString();var newFileName = fileId + extension;var savePath = Path.Combine(uploadFolder, newFileName); File.Move(tempPath, savePath); savedFiles.Add(savePath);var fileUrl =$"{baseUrl}{relativePath}/{newFileName}"; resultFiles.Add(new{ FileId = fileId, FilePath = fileUrl });}returnSuccess(data: resultFiles);}catch(Exception ex){foreach(var savedFile in savedFiles){try{ File.Delete(savedFile);}catch{}}foreach(var fileData in provider.FileData){try{ File.Delete(fileData.LocalFileName);}catch{}}returnFail($"文件上传失败: {ex.Message}");}}[HttpPost]publicIHttpActionResultGetFilePaths(string[] fileIds){var baseUrl =$"{Request.RequestUri.Scheme}://{Request.RequestUri.Authority}";var relativePath = folder.TrimStart('~');var resultFiles =newList<object>();foreach(string fileId in fileIds){var filePath = Directory.GetFiles(uploadFolder, fileId +".*").FirstOrDefault();if(filePath ==null)returnFail($"文件不存在{fileId}");var fileName = Path.GetFileName(filePath); resultFiles.Add(new{ FileId = fileId, FilePath =$"{baseUrl}{relativePath}/{fileName}"});}returnSuccess(data: resultFiles);}[HttpPost]publicIHttpActionResultDeleteFiles(string[] fileIds){if(fileIds ==null|| fileIds.Length ==0)returnFail("请提供要删除的文件ID");using(var scope =newTransactionScope()){foreach(var fileId in fileIds){var filePath = Directory.GetFiles(uploadFolder, fileId +".*").SingleOrDefault();if(filePath ==null)returnFail($"文件不存在: {fileId}"); File.Delete(filePath);} scope.Complete();}returnSuccess();}}

2、文件服务API接口调用工具类

publicclassFileUploader{privatestring fileServiceUrl;privateHttpClient http;publicFileUploader(string fileServiceUrl){this.fileServiceUrl = fileServiceUrl; http =newHttpClient();}publicasyncTask<dynamic>Upload(MultipartFileData[] files){using(var form =newMultipartFormDataContent()){foreach(var file in files){var fileBytes = File.ReadAllBytes(file.LocalFileName);var content =newByteArrayContent(fileBytes); form.Add(content,"file", file.Headers.ContentDisposition.FileName.Trim('\"'));}var url = fileServiceUrl +"/api/File/UploadFile";var response =await http.PostAsync(url, form);var json =await response.Content.ReadAsStringAsync();return Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);}}publicasyncTask<dynamic>GetFilePaths(string[] fileIds){var url = fileServiceUrl +"/api/File/GetFilePaths";var content =newStringContent(Newtonsoft.Json.JsonConvert.SerializeObject(fileIds), Encoding.UTF8,"application/json");var response =await http.PostAsync(url, content);var json =await response.Content.ReadAsStringAsync();return Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);}publicasyncTask<dynamic>DeleteFiles(string[] fileIds){var url = fileServiceUrl +"/api/File/DeleteFiles";var content =newStringContent(Newtonsoft.Json.JsonConvert.SerializeObject(fileIds), Encoding.UTF8,"application/json");var response =await http.PostAsync(url, content);var json =await response.Content.ReadAsStringAsync();return Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);}}

服务调用示例

1、我设计的都是通过后端调文件服务接口,前端不直接调用文件服务
2、文件上传都是先保存在系统临时目录,所以一定要记得清除啊。。。
3、前端示例代码Vue2写的
4、有些细枝末节的都没有粘了啊,应该可以看懂,EF自己配一下

1、后端API接口

publicclassMyTestController:ApiController{privateFileUploader fileUploader;publicMyTestController(){ fileUploader =newFileUploader(ConfigurationManager.AppSettings["FileServiceUrl"]);}[HttpPost]publicasyncTask<IHttpActionResult>UploadTest(){var provider =newMultipartFormDataStreamProvider(Path.GetTempPath());await Request.Content.ReadAsMultipartAsync(provider);if(!provider.FileData.Any())returnBadRequest("没有上传文件");try{var result =await fileUploader.Upload(provider.FileData.ToArray());returnOk(result);}catch(Exception ex){returnBadRequest(ex.Message);}finally{foreach(var file in provider.FileData){if(!File.Exists(file.LocalFileName))continue; File.Delete(file.LocalFileName);}}}[HttpGet]publicasyncTask<IHttpActionResult>QueryUserDatas(){user[] users;using(var dbContext =newjonas_testEntities()) users = dbContext.user.ToArray();var result =await fileUploader.GetFilePaths(users.Select(u => u.IconImageId).ToArray());if(result.Code !=0)returnJson(result);var fileInfos =((IEnumerable<dynamic>)result.Data).Select(d =>new{ d.FileId, d.FilePath }).ToArray();List<QueryUserDatasResultItem> list =newList<QueryUserDatasResultItem>();foreach(var user in users){QueryUserDatasResultItem model =newQueryUserDatasResultItem{ Account = user.Account, RealName = user.RealName }; model.IconImageUrl = fileInfos.Single(s => s.FileId == user.IconImageId).FilePath; list.Add(model);}returnJson(list);}[HttpPost]publicasyncTask<IHttpActionResult>AddUser(){if(!Request.Content.IsMimeMultipartContent())returnJson(new{ Code =1, Message ="请求格式错误"});var provider =newMultipartFormDataStreamProvider(Path.GetTempPath());await Request.Content.ReadAsMultipartAsync(provider);try{var formData = provider.FormData;var user =newuser{ Account = formData["Account"], RealName = formData["RealName"]};if(provider.FileData.Count >0){var fileData = provider.FileData[0];var result =await fileUploader.Upload(new[]{ fileData });if(result.Code !=0)returnJson(result); user.IconImageId = result.Data[0].FileId;}using(var dbContext =newjonas_testEntities()){if(dbContext.user.Any(u => u.Account == user.Account))returnJson(new{ Code=1,Message="账号已经存在"}); dbContext.user.Add(user);await dbContext.SaveChangesAsync();}returnJson(new{ Code =0});}catch(Exception ex){returnJson(new{ Code =1, Message = ex.Message });}finally{foreach(var fileData in provider.FileData){try{if(File.Exists(fileData.LocalFileName)){ File.Delete(fileData.LocalFileName);}}catch{}}}}[HttpPost]publicasyncTask<IHttpActionResult>DeleteUser(string account){user user =null;using(var dbContext =newjonas_testEntities()){ user = dbContext.user.SingleOrDefault(u => u.Account == account);if(user ==null)returnJson(new{ Code =1, Message ="用户不存在"}); dbContext.user.Remove(user);await dbContext.SaveChangesAsync();}await fileUploader.DeleteFiles(newstring[]{ user.IconImageId });returnJson(new{ Code =0});}}

2、前端代码(Vue2+ElementUI实现)

<template><div id="app"><el-table :data="tableDatas" style="width: 800px;" stripe border><el-table-column label="账号" prop="Account" width="150"></el-table-column><el-table-column label="真实姓名" prop="RealName" width="150"></el-table-column><el-table-column label="头像" width="100"><template slot-scope="scope"><span style="color: #409EFF; cursor: pointer;" @click="previewIconHandler(scope.row.IconImageUrl)">预览</span></template></el-table-column><el-table-column width="150"><template #header><el-button type="primary" size="medium" @click="dialogAdd.show = true">新增</el-button></template><template slot-scope="scope"><el-button type="danger" size="small" @click="removeUserHandler(scope.row.Account)">删除</el-button></template></el-table-column></el-table><el-dialog title="预览头像" width="500px":visible.sync="dialogPreviewIcon.show":append-to-body="true":modal-append-to-body="false":lock-scroll="false"><div style="width: 100%;height: 500px;"><img :src="dialogPreviewIcon.src" style="width: 100%;height: 100%;"></div></el-dialog><el-dialog title="新增用户" width="800px":visible.sync="dialogAdd.show":append-to-body="true":modal-append-to-body="false":lock-scroll="false"><el-form :model="dialogAdd.userInfo" label-width="150px"class="demo-ruleForm"><el-form-item label="账号"><el-input v-model="dialogAdd.userInfo.Account"></el-input></el-form-item><el-form-item label="真实姓名"><el-input v-model="dialogAdd.userInfo.RealName"></el-input></el-form-item><el-form-item label="头像"><el-upload class="upload-demo" ref="upload" action="":on-remove="removeOrChangeHandler":file-list="dialogAdd.files":auto-upload="false":on-change="removeOrChangeHandler"><el-button slot="trigger" size="small" type="primary">选取文件</el-button></el-upload></el-form-item><el-form-item><el-button type="primary" @click="submitFormHandler">提交</el-button></el-form-item></el-form></el-dialog></div></template><script>import axios from'axios';const baseUrl ='http://localhost:4623'exportdefault{data(){return{tableDatas:[],dialogPreviewIcon:{show:false,src:''},dialogAdd:{show:false,userInfo:{Account:'',RealName:''},files:[]}}},methods:{asyncqueryDatas(){const{data: res }=await axios.get(baseUrl +'/api/MyTest/QueryUserDatas')this.tableDatas = res },previewIconHandler(IconImageUrl){this.dialogPreviewIcon.src = IconImageUrl this.dialogPreviewIcon.show =true},asyncsubmitFormHandler(){ console.log(this.dialogAdd)if(this.dialogAdd.userInfo.Account ===''||this.dialogAdd.userInfo.RealName ===''||this.dialogAdd.files.length ===0)returnalert('请填写完整信息')let formdata =newFormData() formdata.append('file',this.dialogAdd.files[0].raw) formdata.append('Account',this.dialogAdd.userInfo.Account) formdata.append('RealName',this.dialogAdd.userInfo.RealName)const{data: res }=await axios.post(baseUrl +'/api/MyTest/AddUser', formdata,{headers:{'Content-Type':'multipart/form-data'}})if(res.Code !=0)returnalert(res.Message)this.dialogAdd.show =falsethis.queryDatas()},asyncremoveUserHandler(account){const{data: res }=await axios.post(baseUrl +'/api/MyTest/DeleteUser?account='+ account)if(res.Code !=0)returnalert(res.Message)this.queryDatas()},removeOrChangeHandler(file, files){this.dialogAdd.files = files },},created(){this.queryDatas()}}</script><style lang="less"></style>

3、示例效果

在这里插入图片描述


在这里插入图片描述

Read more

前端 SSG:别让你的网站加载速度慢得像蜗牛

前端 SSG:别让你的网站加载速度慢得像蜗牛 毒舌时刻 这网站加载速度慢得能让我泡杯咖啡回来还没好。 各位前端同行,咱们今天聊聊前端 SSG(静态站点生成)。别告诉我你还在使用纯客户端渲染,那感觉就像在没有预加载的情况下开车——能开,但起步慢得要命。 为什么你需要 SSG 最近看到一个项目,每次加载都要重新获取数据,用户体验差。我就想问:你是在做网站还是在做实时应用? 反面教材 // 反面教材:纯客户端渲染 // App.jsx import React, { useState, useEffect } from 'react'; function App() { const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { async function fetchPosts() { setLoading(

2026年最新全球AI大模型深度研究报告

2026年最新全球AI大模型深度研究报告 文章目录 * 2026年最新全球AI大模型深度研究报告 * 摘要 * 第一章 全球AI大模型发展概况 * 1.1 全球AI发展格局:中美双极引领 * 1.2 市场规模与增长趋势 * 1.3 发展阶段特征 * 第二章 核心技术突破分析 * 2.1 主流大模型最新进展 * 2.1.1 美国阵营 * 2.1.2 中国阵营 * 2.2 核心技术突破方向 * 2.2.1 多模态能力 * 2.2.2 长上下文处理 * 2.2.3 推理能力 * 2.2.4 Agent能力 * 2.2.

AI全栈之路:Cursor+Claude3.7一整套APP原型图UI生成

AI全栈之路:Cursor+Claude3.7一整套APP原型图UI生成

背景 对于大部分工作三年的开发者来说,技术栈不在是瓶颈,从一门语言到另一个门语言,从一个技术栈到另一个技术栈,只需要投入一两周的时间就可以快速入门,从前端、移动端到后端,甚至数据分析,算法,从TS、Java、GO到C++,有了一门技术的基础再学习另一门技术会快很多,很多时候缺乏的是实战了和规模化的用户经验,不过在AI时代,这都不是问题了。 AI IDE(Cursor、trae)可以让我们在只使用自然语言描述需求后快速帮助我们实现对应端的工程代码,还可以帮助我们实现部署。技术上的问题绝大部分都可以通过AI帮助我们解决。卡在我们全栈路上的另一个问题是UI设计稿的问题。对于前端或者移动端开发来讲,尤其是移动端,强依赖设计稿帮我们实现美观的应用。但是从工程师到UI设计师的迁移跨度就太大了,一时半会没有办法快速迁移。目前市面上虽然也有一些生成设计稿的AI工具,但是效果差强人意。本文我们介绍一种“曲线救国”方式的设计稿生成方式,帮助我们快速生成UI设计稿,进一步实现全栈开发运营自己的作品之路。 先上一张效果图: Claude 生成骑行应用H5 Claude直接生成Figma等UI设计稿比

【AI 大模型】LangChain 框架 ① ( LangChain 简介 | LangChain 模块 | LangChain 文档 )

【AI 大模型】LangChain 框架 ① ( LangChain 简介 | LangChain 模块 | LangChain 文档 )

文章目录 * 一、LangChain 简介 * 1、LangChain 概念 * 2、LangChain 定位 * 3、LangChain 开发语言与应用场景 * 4、LangChain 核心组件 * 5、LangChain 学习路径 * 二、LangChain 模块 * 1、模型输入 / 输出 ( Models ) * 2、提示词模板 ( Prompts ) * 3、索引 ( Indexes ) * 4、链 ( Chains ) * 5、记忆 ( Memory ) * 6、代理 ( Agents ) * 7、 工具 ( Tools ) * 8、 文档加载器 ( Document Loaders ) * 9、评估