1 // Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. 2 3 using System.Collections.Generic; 4 using System.Globalization; 5 using System.IO; 6 using System.Net.Http.Headers; 7 8 namespace System.Net.Http 9 { 10 /// <summary> 11 /// An <see cref="IMultipartStreamProvider"/> suited for use with HTML file uploads for writing file 12 /// content to a <see cref="FileStream"/>. The stream provider looks at the <b>Content-Disposition</b> header 13 /// field and determines an output <see cref="Stream"/> based on the presence of a <b>filename</b> parameter. 14 /// If a <b>filename</b> parameter is present in the <b>Content-Disposition</b> header field then the body 15 /// part is written to a <see cref="FileStream"/>, otherwise it is written to a <see cref="MemoryStream"/>. 16 /// This makes it convenient to process MIME Multipart HTML Form data which is a combination of form 17 /// data and file content. 18 /// </summary> 19 public class MultipartFormDataStreamProvider : IMultipartStreamProvider 20 { 21 private const int MinBufferSize = 1; 22 private const int DefaultBufferSize = 0x1000; 23 24 private Dictionary<string, string> _bodyPartFileNames = new Dictionary<string, string>(); 25 private readonly object _thisLock = new object(); 26 private string _rootPath; 27 private int _bufferSize = DefaultBufferSize; 28 29 /// <summary> 30 /// Initializes a new instance of the <see cref="MultipartFormDataStreamProvider"/> class. 31 /// </summary> 32 /// <param name="rootPath">The root path where the content of MIME multipart body parts are written to.</param> MultipartFormDataStreamProvider(string rootPath)33 public MultipartFormDataStreamProvider(string rootPath) 34 : this(rootPath, DefaultBufferSize) 35 { 36 } 37 38 /// <summary> 39 /// Initializes a new instance of the <see cref="MultipartFormDataStreamProvider"/> class. 40 /// </summary> 41 /// <param name="rootPath">The root path where the content of MIME multipart body parts are written to.</param> 42 /// <param name="bufferSize">The number of bytes buffered for writes to the file.</param> MultipartFormDataStreamProvider(string rootPath, int bufferSize)43 public MultipartFormDataStreamProvider(string rootPath, int bufferSize) 44 { 45 if (String.IsNullOrWhiteSpace(rootPath)) 46 { 47 throw new ArgumentNullException("rootPath"); 48 } 49 50 if (bufferSize < MinBufferSize) 51 { 52 throw new ArgumentOutOfRangeException("bufferSize", bufferSize, RS.Format(Properties.Resources.ArgumentMustBeGreaterThanOrEqualTo, MinBufferSize)); 53 } 54 55 _rootPath = Path.GetFullPath(rootPath); 56 _bufferSize = bufferSize; 57 } 58 59 /// <summary> 60 /// Gets an <see cref="IDictionary{T1, T2}"/> instance containing mappings of each 61 /// <b>filename</b> parameter provided in a <b>Content-Disposition</b> header field 62 /// (represented as the keys) to a local file name where the contents of the body part is 63 /// stored (represented as the values). 64 /// </summary> 65 public IDictionary<string, string> BodyPartFileNames 66 { 67 get 68 { 69 lock (_thisLock) 70 { 71 return new Dictionary<string, string>(_bodyPartFileNames); 72 } 73 } 74 } 75 76 /// <summary> 77 /// This body part stream provider examines the headers provided by the MIME multipart parser 78 /// and decides whether it should return a file stream or a memory stream for the body part to be 79 /// written to. 80 /// </summary> 81 /// <param name="headers">Header fields describing the body part</param> 82 /// <returns>The <see cref="Stream"/> instance where the message body part is written to.</returns> GetStream(HttpContentHeaders headers)83 public virtual Stream GetStream(HttpContentHeaders headers) 84 { 85 if (headers == null) 86 { 87 throw new ArgumentNullException("headers"); 88 } 89 90 ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition; 91 if (contentDisposition != null) 92 { 93 // If we have a file name then write contents out to temporary file. Otherwise just write to MemoryStream 94 if (!String.IsNullOrEmpty(contentDisposition.FileName)) 95 { 96 string localFilePath; 97 try 98 { 99 string filename = GetLocalFileName(headers); 100 localFilePath = Path.Combine(_rootPath, Path.GetFileName(filename)); 101 } 102 catch (Exception e) 103 { 104 throw new InvalidOperationException(Properties.Resources.MultipartStreamProviderInvalidLocalFileName, e); 105 } 106 107 // Add mapping from Content-Disposition FileName parameter to local file name. 108 lock (_thisLock) 109 { 110 _bodyPartFileNames.Add(contentDisposition.FileName, localFilePath); 111 } 112 113 return File.Create(localFilePath, _bufferSize, FileOptions.Asynchronous); 114 } 115 116 // If no filename parameter was found in the Content-Disposition header then return a memory stream. 117 return new MemoryStream(); 118 } 119 120 // If no Content-Disposition header was present. 121 throw new IOException(RS.Format(Properties.Resources.MultipartFormDataStreamProviderNoContentDisposition, "Content-Disposition")); 122 } 123 124 /// <summary> 125 /// Gets the name of the local file which will be combined with the root path to 126 /// create an absolute file name where the contents of the current MIME body part 127 /// will be stored. 128 /// </summary> 129 /// <param name="headers">The headers for the current MIME body part.</param> 130 /// <returns>A relative filename with no path component.</returns> GetLocalFileName(HttpContentHeaders headers)131 public virtual string GetLocalFileName(HttpContentHeaders headers) 132 { 133 if (headers == null) 134 { 135 throw new ArgumentNullException("headers"); 136 } 137 138 return String.Format(CultureInfo.InvariantCulture, "BodyPart_{0}", Guid.NewGuid()); 139 } 140 } 141 } 142