();
16 | }
17 |
18 | public void OnResourceExecuted(ResourceExecutedContext context)
19 | {
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/ServerSideApp/Pages/Error.cshtml:
--------------------------------------------------------------------------------
1 | @page
2 | @model ErrorModel
3 | @{
4 | ViewData["Title"] = "Error";
5 | }
6 |
7 | Error.
8 | An error occurred while processing your request.
9 |
10 | @if (Model.ShowRequestId)
11 | {
12 |
13 | Request ID: @Model.RequestId
14 |
15 | }
16 |
17 | Development Mode
18 |
19 | Swapping to the Development environment displays detailed information about the error that occurred.
20 |
21 |
22 | The Development environment shouldn't be enabled for deployed applications.
23 | It can result in displaying sensitive information from exceptions to end users.
24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
25 | and restarting the app.
26 |
27 |
--------------------------------------------------------------------------------
/ServerSideApp/Pages/Error.cshtml.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using Microsoft.AspNetCore.Mvc;
3 | using Microsoft.AspNetCore.Mvc.RazorPages;
4 |
5 | namespace ServerSideApp.Pages
6 | {
7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
8 | public class ErrorModel : PageModel
9 | {
10 | public string RequestId { get; set; }
11 |
12 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
13 |
14 | public void OnGet()
15 | {
16 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/ServerSideApp/Pages/Index.cshtml:
--------------------------------------------------------------------------------
1 | @page
2 | @model StreamedSingleFileUploadPhysicalModel
3 | @{
4 | ViewData["Title"] = "Streamed Single File Upload with AJAX (Physical)";
5 | }
6 |
7 | Stream a file with AJAX to physical storage with a controller endpoint
8 |
9 | The following form's action
points to a controller endpoint that only receives the file and saves it to disk. If additional form data is added to the form, the additional data is ignored by the controller action.
10 |
11 |
28 |
29 | @section Scripts {
30 |
58 | }
59 |
--------------------------------------------------------------------------------
/ServerSideApp/Pages/Index.cshtml.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.RazorPages;
2 |
3 | namespace ServerSideApp.Pages
4 | {
5 | public class StreamedSingleFileUploadPhysicalModel : PageModel
6 | {
7 | public void OnGet()
8 | {
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/ServerSideApp/Pages/Shared/_Layout.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | @ViewData["Title"]
7 |
8 |
9 |
10 |
11 |
12 | @RenderBody()
13 |
14 |
15 | @RenderSection("Scripts", required: false)
16 |
17 |
18 |
--------------------------------------------------------------------------------
/ServerSideApp/Pages/_ViewImports.cshtml:
--------------------------------------------------------------------------------
1 | @namespace ServerSideApp.Pages
2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
3 |
--------------------------------------------------------------------------------
/ServerSideApp/Pages/_ViewStart.cshtml:
--------------------------------------------------------------------------------
1 | @{
2 | Layout = "_Layout";
3 | }
4 |
--------------------------------------------------------------------------------
/ServerSideApp/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Microsoft.Extensions.Configuration;
7 | using Microsoft.Extensions.Hosting;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace ServerSideApp
11 | {
12 | public class Program
13 | {
14 | public static void Main(string[] args)
15 | {
16 | CreateHostBuilder(args).Build().Run();
17 | }
18 |
19 | public static IHostBuilder CreateHostBuilder(string[] args) =>
20 | Host.CreateDefaultBuilder(args)
21 | .ConfigureWebHostDefaults(webBuilder =>
22 | {
23 | webBuilder.UseStartup();
24 | });
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ServerSideApp/ServerSideApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ServerSideApp/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Hosting;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Hosting;
6 | using ServerSideApp.Filters;
7 |
8 | namespace ServerSideApp
9 | {
10 | public class Startup
11 | {
12 | public Startup(IConfiguration configuration)
13 | {
14 | Configuration = configuration;
15 | }
16 |
17 | public IConfiguration Configuration { get; }
18 |
19 | // This method gets called by the runtime. Use this method to add services to the container.
20 | public void ConfigureServices(IServiceCollection services)
21 | {
22 | services.AddControllers();
23 |
24 | #region snippet_AddRazorPages
25 | services.AddRazorPages()
26 | .AddRazorPagesOptions(options =>
27 | {
28 | options.Conventions
29 | .AddPageApplicationModelConvention("/Index", model =>
30 | {
31 | model.Filters.Add(new DisableFormValueModelBindingAttribute());
32 | });
33 | });
34 | #endregion
35 |
36 | }
37 |
38 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
39 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
40 | {
41 | if (env.IsDevelopment())
42 | {
43 | app.UseDeveloperExceptionPage();
44 | }
45 |
46 | app.UseRouting();
47 |
48 | app.UseStaticFiles();
49 | app.UseRouting();
50 |
51 | app.UseEndpoints(endpoints => {
52 | endpoints.MapControllers();
53 | endpoints.MapRazorPages();
54 | });
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/ServerSideApp/Utilities/FileHelpers.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Mvc.ModelBinding;
7 | using Microsoft.AspNetCore.WebUtilities;
8 | using Microsoft.Net.Http.Headers;
9 |
10 | namespace ServerSideApp.Utilities
11 | {
12 | public static class FileHelpers
13 | {
14 | // If you require a check on specific characters in the IsValidFileExtensionAndSignature
15 | // method, supply the characters in the _allowedChars field.
16 | private static readonly byte[] AllowedChars = { };
17 | // For more file signatures, see the File Signatures Database (https://www.filesignatures.net/)
18 | // and the official specifications for the file types you wish to add.
19 | private static readonly Dictionary> FileSignature = new Dictionary>
20 | {
21 | { ".gif", new List { new byte[] { 0x47, 0x49, 0x46, 0x38 } } },
22 | { ".png", new List { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } },
23 | { ".jpeg", new List
24 | {
25 | new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
26 | new byte[] { 0xFF, 0xD8, 0xFF, 0xE2 },
27 | new byte[] { 0xFF, 0xD8, 0xFF, 0xE3 },
28 | }
29 | },
30 | { ".jpg", new List
31 | {
32 | new byte[] { 0xFF, 0xD8, 0xFF, 0xE0 },
33 | new byte[] { 0xFF, 0xD8, 0xFF, 0xE1 },
34 | new byte[] { 0xFF, 0xD8, 0xFF, 0xE8 },
35 | }
36 | },
37 | { ".zip", new List
38 | {
39 | new byte[] { 0x50, 0x4B, 0x03, 0x04 },
40 | new byte[] { 0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45 },
41 | new byte[] { 0x50, 0x4B, 0x53, 0x70, 0x58 },
42 | new byte[] { 0x50, 0x4B, 0x05, 0x06 },
43 | new byte[] { 0x50, 0x4B, 0x07, 0x08 },
44 | new byte[] { 0x57, 0x69, 0x6E, 0x5A, 0x69, 0x70 },
45 | }
46 | },
47 | };
48 |
49 |
50 | public static async Task ProcessStreamedFile(
51 | MultipartSection section, ContentDispositionHeaderValue contentDisposition,
52 | ModelStateDictionary modelState, string[] permittedExtensions, long sizeLimit)
53 | {
54 | try
55 | {
56 | using (var memoryStream = new MemoryStream())
57 | {
58 | await section.Body.CopyToAsync(memoryStream);
59 |
60 | // Check if the file is empty or exceeds the size limit.
61 | if (memoryStream.Length == 0)
62 | {
63 | modelState.AddModelError("File", "The file is empty.");
64 | }
65 | else if (memoryStream.Length > sizeLimit)
66 | {
67 | var megabyteSizeLimit = sizeLimit / 1048576;
68 | modelState.AddModelError("File", $"The file exceeds {megabyteSizeLimit:N1} MB.");
69 | }
70 | else if (!IsValidFileExtensionAndSignature(contentDisposition.FileName.Value, memoryStream, permittedExtensions))
71 | {
72 | modelState.AddModelError("File", "The file type isn't permitted or the file's signature doesn't match the file's extension.");
73 | }
74 | else
75 | {
76 | return memoryStream.ToArray();
77 | }
78 | }
79 | }
80 | catch (Exception ex)
81 | {
82 | modelState.AddModelError("File", $"The upload failed. Please contact the Help Desk for support. Error: {ex.HResult}");
83 | // Log the exception
84 | }
85 |
86 | return new byte[0];
87 | }
88 |
89 | private static bool IsValidFileExtensionAndSignature(string fileName, Stream data, IEnumerable permittedExtensions)
90 | {
91 | if (string.IsNullOrEmpty(fileName) || data == null || data.Length == 0)
92 | {
93 | return false;
94 | }
95 |
96 | var ext = Path.GetExtension(fileName).ToLowerInvariant();
97 |
98 | if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
99 | {
100 | return false;
101 | }
102 |
103 | data.Position = 0;
104 |
105 | using (var reader = new BinaryReader(data))
106 | {
107 | if (ext.Equals(".txt") || ext.Equals(".csv") || ext.Equals(".prn"))
108 | {
109 | if (AllowedChars.Length == 0)
110 | {
111 | // Limits characters to ASCII encoding.
112 | for (var i = 0; i < data.Length; i++)
113 | {
114 | if (reader.ReadByte() > sbyte.MaxValue)
115 | {
116 | return false;
117 | }
118 | }
119 | }
120 | else
121 | {
122 | // Limits characters to ASCII encoding and
123 | // values of the _allowedChars array.
124 | for (var i = 0; i < data.Length; i++)
125 | {
126 | var b = reader.ReadByte();
127 | if (b > sbyte.MaxValue ||
128 | !AllowedChars.Contains(b))
129 | {
130 | return false;
131 | }
132 | }
133 | }
134 |
135 | return true;
136 | }
137 |
138 | // Uncomment the following code block if you must permit
139 | // files whose signature isn't provided in the _fileSignature
140 | // dictionary. We recommend that you add file signatures
141 | // for files (when possible) for all file types you intend
142 | // to allow on the system and perform the file signature
143 | // check.
144 | /*
145 | if (!_fileSignature.ContainsKey(ext))
146 | {
147 | return true;
148 | }
149 | */
150 |
151 | // File signature check
152 | // --------------------
153 | // With the file signatures provided in the _fileSignature
154 | // dictionary, the following code tests the input content's
155 | // file signature.
156 | var signatures = FileSignature[ext];
157 | var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
158 |
159 | return signatures.Any(signature =>
160 | headerBytes.Take(signature.Length).SequenceEqual(signature));
161 | }
162 | }
163 | }
164 | }
--------------------------------------------------------------------------------
/ServerSideApp/Utilities/MultipartRequestHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using Microsoft.Net.Http.Headers;
4 |
5 | namespace ServerSideApp.Utilities
6 | {
7 | public static class MultipartRequestHelper
8 | {
9 | // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
10 | // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters is a reasonable limit.
11 | public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
12 | {
13 | var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
14 |
15 | if (string.IsNullOrWhiteSpace(boundary))
16 | {
17 | throw new InvalidDataException("Missing content-type boundary.");
18 | }
19 |
20 | if (boundary.Length > lengthLimit)
21 | {
22 | throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded.");
23 | }
24 |
25 | return boundary;
26 | }
27 |
28 | public static bool IsMultipartContentType(string contentType)
29 | {
30 | return !string.IsNullOrEmpty(contentType)
31 | && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ServerSideApp/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/ServerSideApp/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft": "Warning",
6 | "Microsoft.Hosting.Lifetime": "Information"
7 | }
8 | },
9 | "AllowedHosts": "*",
10 | "StoredFilesPath": "c:\\temp",
11 | "FileSizeLimit": 2097152
12 | }
13 |
--------------------------------------------------------------------------------
/httpclient-file-upload-download.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dotnet-labs/FileTransferUsingHttpClient/1009954e315c4da3e9a21a2f8ece408c38817406/httpclient-file-upload-download.gif
--------------------------------------------------------------------------------