问题描述
|
我们正在通过ASP.NET中的ASHX处理程序提供应用程序设置的下载。
一位客户告诉我们,他使用了一些第三方下载管理器应用程序,而我们提供文件的方式目前不支持他的下载管理器应用程序的“恢复”功能。
我的问题是:
恢复下载的基本原理是什么?是否有某些HTTP GET请求告诉我开始的偏移量?
解决方法
恢复下载通常可以通过HTTP“ 0”头进行。例如,如果客户端只需要文件的第二个千字节,则它可能会发送标头“ 1”。
有关更多信息,请参见RFC的HTTP / 1.1页139。
, 感谢icktoofay让我入门,这是一个完整的示例,可以节省其他开发人员一些时间:
磁盘示例
/// <summary>
/// Writes the file stored in the filesystem to the response stream without buffering in memory,ideal for large files. Supports resumable downloads.
/// </summary>
/// <param name=\"filename\">The name of the file to write to the HTTP output.</param>
/// <param name=\"etag\">A unique identifier for the content. Required for IE9 resumable downloads,must be a strong etag which means begins and ends in a quote i.e. \"\\\"6c132-941-ad7e3080\\\"\"</param>
public static void TransmitFile(this HttpResponse response,string filename,string etag)
{
var request = HttpContext.Current.Request;
var fileInfo = new FileInfo(filename);
var responseLength = fileInfo.Exists ? fileInfo.Length : 0;
var buffer = new byte[4096];
var startIndex = 0;
//if the \"If-Match\" exists and is different to etag (or is equal to any \"*\" with no resource) then return 412 precondition failed
if (request.Headers[\"If-Match\"] == \"*\" && !fileInfo.Exists ||
request.Headers[\"If-Match\"] != null && request.Headers[\"If-Match\"] != \"*\" && request.Headers[\"If-Match\"] != etag)
{
response.StatusCode = (int)HttpStatusCode.PreconditionFailed;
response.End();
}
if (!fileInfo.Exists)
{
response.StatusCode = (int)HttpStatusCode.NotFound;
response.End();
}
if (request.Headers[\"If-None-Match\"] == etag)
{
response.StatusCode = (int)HttpStatusCode.NotModified;
response.End();
}
if (request.Headers[\"Range\"] != null && (request.Headers[\"If-Range\"] == null || request.Headers[\"IF-Range\"] == etag))
{
var match = Regex.Match(request.Headers[\"Range\"],@\"bytes=(\\d*)-(\\d*)\");
startIndex = Parse<int>(match.Groups[1].Value);
responseLength = (Parse<int?>(match.Groups[2].Value) + 1 ?? fileInfo.Length) - startIndex;
response.StatusCode = (int)HttpStatusCode.PartialContent;
response.Headers[\"Content-Range\"] = \"bytes \" + startIndex + \"-\" + (startIndex + responseLength - 1) + \"/\" + fileInfo.Length;
}
response.Headers[\"Accept-Ranges\"] = \"bytes\";
response.Headers[\"Content-Length\"] = responseLength.ToString();
response.Cache.SetCacheability(HttpCacheability.Public); //required for etag output
response.Cache.SetETag(etag); //required for IE9 resumable downloads
response.TransmitFile(filename,startIndex,responseLength);
}
public void ProcessRequest(HttpContext context)
{
var id = Parse<int>(context.Request.QueryString[\"id\"]);
var version = context.Request.QueryString[\"v\"];
var db = new DataClassesDataContext();
var filePath = db.Documents.Where(d => d.ID == id).Select(d => d.Fullpath).FirstOrDefault();
if (String.IsNullOfEmpty(filePath) || !File.Exists(filePath))
{
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
context.Response.End();
}
context.Response.AddHeader(\"content-disposition\",\"filename=\" + Path.GetFileName(filePath));
context.Response.ContentType = GetMimeType(filePath);
context.Response.TransmitFile(filePath,version);
}
数据库实例
/// <summary>
/// Writes the file stored in the database to the response stream without buffering in memory,ideal for large files. Supports resumable downloads.
/// </summary>
/// <param name=\"retrieveBinarySql\">The sql to retrieve the binary data of the file from the database to be transmitted to the client. Parameters can be reffered to by {0} the index in the supplied parameter array.</param>
/// <param name=\"retrieveBinarySqlParameters\">The parameters used in the sql query. Specify null if no parameters are required.</param>
/// <param name=\"connectionString\">The connectring string for the sql database.</param>
/// <param name=\"contentLength\">The length of the content in bytes.</param>
/// <param name=\"etag\">A unique identifier for the content. Required for IE9 resumable downloads,must be a strong etag which means begins and ends in a quote i.e. \"\\\"6c132-941-ad7e3080\\\"\"</param>
/// <param name=\"useFilestream\">If the binary data is stored using Sql\'s Filestream feature set this to true to stream the file directly.</param>
public static void TransmitFile(this HttpResponse response,string retrieveBinarySql,object[] retrieveBinarySqlParameters,string connectionString,int contentLength,string etag,bool useFilestream)
{
var request = HttpContext.Current.Request;
var responseLength = contentLength;
var buffer = new byte[4096];
var startIndex = 0;
//if the \"If-Match\" exists and is different to etag (or is equal to any \"*\" with no resource) then return 412 precondition failed
if (request.Headers[\"If-Match\"] == \"*\" && contentLength == 0 ||
request.Headers[\"If-Match\"] != null && request.Headers[\"If-Match\"] != \"*\" && request.Headers[\"If-Match\"] != etag)
{
response.StatusCode = (int)HttpStatusCode.PreconditionFailed;
response.End();
}
if (contentLength == 0)
{
response.StatusCode = (int)HttpStatusCode.NotFound;
response.End();
}
if (request.Headers[\"If-None-Match\"] == etag)
{
response.StatusCode = (int)HttpStatusCode.NotModified;
response.End();
}
if (request.Headers[\"Range\"] != null && (request.Headers[\"If-Range\"] == null || request.Headers[\"IF-Range\"] == etag))
{
var match = Regex.Match(request.Headers[\"Range\"],@\"bytes=(\\d*)-(\\d*)\");
startIndex = Parse<int>(match.Groups[1].Value);
responseLength = (Parse<int?>(match.Groups[2].Value) + 1 ?? contentLength) - startIndex;
response.StatusCode = (int)HttpStatusCode.PartialContent;
response.Headers[\"Content-Range\"] = \"bytes \" + startIndex + \"-\" + (startIndex + responseLength - 1) + \"/\" + contentLength;
}
response.Headers[\"Accept-Ranges\"] = \"bytes\";
response.Headers[\"Content-Length\"] = responseLength.ToString();
response.Cache.SetCacheability(HttpCacheability.Public); //required for etag output
response.Cache.SetETag(etag); //required for IE9 resumable downloads
response.BufferOutput = false; //don\'t load entire data into memory (buffer) before sending
if (!useFilestream)
{
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
var command = new SqlCommand(retrieveBinarySql,connection);
for (var i = 0; retrieveBinarySqlParameters != null && i < retrieveBinarySqlParameters.Length; i++)
{
command.Parameters.AddWithValue(\"p\" + i,retrieveBinarySqlParameters[i]);
command.CommandText = command.CommandText.Replace(\"{\" + i + \"}\",\"@p\" + i);
}
var reader = command.ExecuteReader(CommandBehavior.SequentialAccess);
if (!reader.Read())
{
response.StatusCode = (int)HttpStatusCode.NotFound;
response.End();
}
for (var i = startIndex; i < contentLength; i += buffer.Length)
{
var bytesRead = (int)reader.GetBytes(0,i,buffer,buffer.Length);
response.OutputStream.Write(buffer,bytesRead);
}
}
}
else
{
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
var tran = connection.BeginTransaction(IsolationLevel.ReadCommitted);
var command = new SqlCommand(Regex.Replace(retrieveBinarySql,@\"select \\w+ \",v => v.Value.TrimEnd() + \".PathName(),GET_FILESTREAM_TRANSACTION_CONTEXT() \"),connection);
command.Transaction = tran;
for (var i = 0; retrieveBinarySqlParameters != null && i < retrieveBinarySqlParameters.Length; i++)
{
command.Parameters.AddWithValue(\"p\" + i,\"@p\" + i);
}
var reader = command.ExecuteReader();
if (!reader.Read())
{
response.StatusCode = (int)HttpStatusCode.NotFound;
response.End();
}
var path = reader.GetString(0);
var transactionContext = (byte[])reader.GetValue(1);
using (var fileStream = new SqlFileStream(path,transactionContext,FileAccess.Read,FileOptions.SequentialScan,0))
{
fileStream.Seek(startIndex,SeekOrigin.Begin);
int bytesRead;
do
{
bytesRead = fileStream.Read(buffer,buffer.Length);
response.OutputStream.Write(buffer,bytesRead);
}
while (bytesRead == buffer.Length);
}
tran.Commit();
}
}
}
public void ProcessRequest(HttpContext context)
{
var id = Parse<int>(context.Request.QueryString[\"id\"]);
var db = new DataClassesDataContext();
var doc = db.Documents.Where(d => d.ID == id).Select(d => new { d.Data.Length,d.Filename,d.Version }).FirstOrDefault();
if (doc == null)
{
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
context.Response.End();
}
context.Response.AddHeader(\"content-disposition\",\"filename=\" + doc.Filename);
context.Response.ContentType = GetMimeType(doc.Filename);
context.Response.TransmitFile(\"select data from documents where id = {0}\",new[] { id },db.Connection.ConnectionString,doc.Length,doc.Version,false);
}
辅助方法
public static T Parse<T>(object value)
{
//convert value to string to allow conversion from types like float to int
//converter.IsValid only works since .NET4 but still returns invalid values for a few cases like NULL for Unit and not respecting locale for date validation
try { return (T)System.ComponentModel.TypeDescriptor.GetConverter(typeof(T)).ConvertFrom(value.ToString()); }
catch (Exception) { return default(T); }
}
public string GetMimeType(string fileName)
{
//note use version 2.0.0.0 if .NET 4 is not installed,in .NET 4.5 this method has now been made public,this method apparently stores a list of mime types which would be more complete then using registry
return (string)Assembly.Load(\"System.Web,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b03f5f7f11d50a3a\")
.GetType(\"System.Web.MimeMapping\")
.GetMethod(\"GetMimeMapping\",BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)
.Invoke(null,new object[] { fileName });
}
这说明了一种从磁盘或数据库中读取部分文件并作为响应输出而不是将整个文件加载到内存中的方法,如果在下载过程中途暂停或恢复下载,则会浪费资源。
编辑:添加了etag以在IE9中启用可恢复的下载,这要感谢EricLaw为使它在IE9中正常工作所提供的帮助。