Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 0974c69

Browse files
committed
[Fixes #429] FileResult
1) Implemented FilePathResult to efficiently return files from disk. 2) Implemented FileStreamResult to return content from a stream. 3) Implemented FileContentResult to return content from a byte array.
1 parent 6279229 commit 0974c69

File tree

12 files changed

+914
-0
lines changed

12 files changed

+914
-0
lines changed

samples/MvcSample.Web/HomeController.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public ActionResult NotFound()
3434
return HttpNotFound();
3535
}
3636

37+
public ActionResult SendFileFromDisk()
38+
{
39+
return File("sample.txt", "text/plain");
40+
}
41+
3742
public bool IsDefaultNameSpace()
3843
{
3944
var namespaceToken = ActionContext.RouteData.DataTokens["NameSpace"] as string;

samples/MvcSample.Web/sample.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is a sample file returned from a controller
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNet.Http;
5+
using System;
6+
using System.Threading.Tasks;
7+
8+
namespace Microsoft.AspNet.Mvc
9+
{
10+
public class FileContentResult : FileResult
11+
{
12+
public FileContentResult(byte[] fileContents, string contentType)
13+
: base(contentType)
14+
{
15+
if (fileContents == null)
16+
{
17+
throw new ArgumentNullException("fileContents");
18+
}
19+
20+
FileContents = fileContents;
21+
}
22+
23+
public byte[] FileContents { get; private set; }
24+
25+
protected async override Task WriteFileAsync(HttpResponse response)
26+
{
27+
await response.Body.WriteAsync(FileContents, 0, FileContents.Length);
28+
}
29+
}
30+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNet.Http;
5+
using Microsoft.AspNet.HttpFeature;
6+
using System;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
10+
namespace Microsoft.AspNet.Mvc
11+
{
12+
public class FilePathResult : FileResult
13+
{
14+
public FilePathResult([NotNull]string fileName, [NotNull]string contentType)
15+
: base(contentType)
16+
{
17+
FileName = fileName;
18+
}
19+
20+
public string FileName { get; private set; }
21+
22+
protected async override Task WriteFileAsync(HttpResponse response)
23+
{
24+
var sendFile = response.HttpContext.GetFeature<IHttpSendFileFeature>();
25+
await sendFile.SendFileAsync(FileName, 0, null, CancellationToken.None);
26+
}
27+
}
28+
}
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNet.Http;
5+
using System;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace Microsoft.AspNet.Mvc
10+
{
11+
public abstract class FileResult : ActionResult
12+
{
13+
private string _fileDownloadName;
14+
15+
protected FileResult([NotNull]string contentType)
16+
{
17+
ContentType = contentType;
18+
}
19+
20+
public string ContentType { get; private set; }
21+
22+
public string FileDownloadName
23+
{
24+
get { return _fileDownloadName ?? string.Empty; }
25+
set { _fileDownloadName = value; }
26+
}
27+
28+
public async override Task ExecuteResultAsync([NotNull]ActionContext context)
29+
{
30+
var response = context.HttpContext.Response;
31+
response.ContentType = ContentType;
32+
33+
if (!string.IsNullOrEmpty(FileDownloadName))
34+
{
35+
// From RFC 2183, Sec. 2.3:
36+
// The sender may want to suggest a filename to be used if the entity is
37+
// detached and stored in a separate file. If the receiving MUA writes
38+
// the entity to a file, the suggested filename should be used as a
39+
// basis for the actual filename, where possible.
40+
string headerValue = ContentDispositionUtil.GetHeaderValue(FileDownloadName);
41+
context.HttpContext.Response.Headers.Set("Content-Disposition", headerValue);
42+
}
43+
44+
await WriteFileAsync(response);
45+
}
46+
47+
protected abstract Task WriteFileAsync(HttpResponse response);
48+
49+
internal static class ContentDispositionUtil
50+
{
51+
private const string HexDigits = "0123456789ABCDEF";
52+
53+
private static void AddByteToStringBuilder(byte b, StringBuilder builder)
54+
{
55+
builder.Append('%');
56+
57+
int i = b;
58+
AddHexDigitToStringBuilder(i >> 4, builder);
59+
AddHexDigitToStringBuilder(i % 16, builder);
60+
}
61+
62+
private static void AddHexDigitToStringBuilder(int digit, StringBuilder builder)
63+
{
64+
builder.Append(HexDigits[digit]);
65+
}
66+
67+
private static string CreateRfc2231HeaderValue(string filename)
68+
{
69+
StringBuilder builder = new StringBuilder("attachment; filename*=UTF-8''");
70+
71+
byte[] filenameBytes = Encoding.UTF8.GetBytes(filename);
72+
foreach (byte b in filenameBytes)
73+
{
74+
if (IsByteValidHeaderValueCharacter(b))
75+
{
76+
builder.Append((char)b);
77+
}
78+
else
79+
{
80+
AddByteToStringBuilder(b, builder);
81+
}
82+
}
83+
84+
return builder.ToString();
85+
}
86+
87+
public static string GetHeaderValue(string fileName)
88+
{
89+
// If fileName contains any Unicode characters, encode according
90+
// to RFC 2231 (with clarifications from RFC 5987)
91+
foreach (char c in fileName)
92+
{
93+
if ((int)c > 127)
94+
{
95+
return CreateRfc2231HeaderValue(fileName);
96+
}
97+
}
98+
99+
return CreateNonUnicodeHeaderValue(fileName);
100+
}
101+
102+
private static string CreateNonUnicodeHeaderValue(string fileName)
103+
{
104+
string escapedFileName = EscapeFileName(fileName);
105+
return string.Format("attachment; filename={0}", escapedFileName);
106+
}
107+
108+
private static string EscapeFileName(string fileName)
109+
{
110+
var hasToBeQuoted = false;
111+
for (int i = 0; i < fileName.Length; i++)
112+
{
113+
if (fileName[i] == '\n')
114+
{
115+
// See RFC 2047 for more details
116+
return GetRfc2047Base64EncodedWord(fileName);
117+
}
118+
119+
// Control characters = (octets 0 - 31) and DEL (127)
120+
if (char.IsControl(fileName[i]))
121+
{
122+
hasToBeQuoted = true;
123+
}
124+
125+
switch (fileName[i])
126+
{
127+
case '(':
128+
case ')':
129+
case '<':
130+
case '>':
131+
case '@':
132+
case ',':
133+
case ';':
134+
case ':':
135+
case '\\':
136+
case '/':
137+
case '[':
138+
case ']':
139+
case '?':
140+
case '=':
141+
case '{':
142+
case '}':
143+
case ' ':
144+
case '\t':
145+
case '"':
146+
hasToBeQuoted = true;
147+
break;
148+
default:
149+
break;
150+
}
151+
}
152+
153+
return hasToBeQuoted ? QuoteFileName(fileName) : fileName;
154+
}
155+
156+
private static string QuoteFileName(string fileName)
157+
{
158+
StringBuilder builder = new StringBuilder();
159+
builder.Append("\"");
160+
161+
for (var i = 0; i < fileName.Length; i++)
162+
{
163+
switch (fileName[i])
164+
{
165+
case '\\':
166+
builder.Append("\\\\");
167+
break;
168+
case '"':
169+
builder.Append("\\\"");
170+
break;
171+
default:
172+
builder.Append(fileName[i]);
173+
break;
174+
}
175+
}
176+
177+
builder.Append("\"");
178+
return builder.ToString();
179+
}
180+
181+
private static string GetRfc2047Base64EncodedWord(string fileName)
182+
{
183+
// See RFC 2047 for details. Section 8 for examples.
184+
const string charset = "utf-8";
185+
const string encoding = "B";
186+
187+
var base64EncodedFileName = Convert.ToBase64String(Encoding.UTF8.GetBytes(fileName));
188+
189+
return string.Format("\"=?{0}?{1}?{2}?=\"", charset, encoding, base64EncodedFileName);
190+
}
191+
192+
193+
// Application of RFC 2231 Encoding to Hypertext Transfer Protocol (HTTP) Header Fields, sec. 3.2
194+
// http://greenbytes.de/tech/webdav/draft-reschke-rfc2231-in-http-latest.html
195+
private static bool IsByteValidHeaderValueCharacter(byte b)
196+
{
197+
if ((byte)'0' <= b && b <= (byte)'9')
198+
{
199+
return true; // is digit
200+
}
201+
if ((byte)'a' <= b && b <= (byte)'z')
202+
{
203+
return true; // lowercase letter
204+
}
205+
if ((byte)'A' <= b && b <= (byte)'Z')
206+
{
207+
return true; // uppercase letter
208+
}
209+
210+
switch (b)
211+
{
212+
case (byte)'-':
213+
case (byte)'.':
214+
case (byte)'_':
215+
case (byte)'~':
216+
case (byte)':':
217+
case (byte)'!':
218+
case (byte)'$':
219+
case (byte)'&':
220+
case (byte)'+':
221+
return true;
222+
}
223+
224+
return false;
225+
}
226+
}
227+
}
228+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.IO;
5+
using System.Threading.Tasks;
6+
using Microsoft.AspNet.Http;
7+
8+
namespace Microsoft.AspNet.Mvc
9+
{
10+
public class FileStreamResult : FileResult
11+
{
12+
// default buffer size as defined in BufferedStream type
13+
private const int BufferSize = 0x1000;
14+
15+
public FileStreamResult([NotNull]Stream fileStream, string contentType)
16+
: base(contentType)
17+
{
18+
19+
FileStream = fileStream;
20+
}
21+
22+
public Stream FileStream { get; private set; }
23+
24+
protected async override Task WriteFileAsync(HttpResponse response)
25+
{
26+
// grab chunks of data and write to the output stream
27+
var outputStream = response.Body;
28+
using (FileStream)
29+
{
30+
byte[] buffer = new byte[BufferSize];
31+
32+
while (true)
33+
{
34+
int bytesRead = await FileStream.ReadAsync(buffer, 0, BufferSize);
35+
if (bytesRead == 0)
36+
{
37+
// no more data
38+
break;
39+
}
40+
41+
await outputStream.WriteAsync(buffer, 0, bytesRead);
42+
}
43+
}
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)