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