您现在的位置是:网站首页> C#技术
C# 大文件数据流分段输出,多段输出,支持视频图片等
- C#技术
- 2023-08-06
- 599人已阅读
为了防止直接请求文件而导致数据被采集,通过接口逻辑判断后再输出文件流的方式模拟完成直接请求文件的操作,支持大文件流操作
C#代码
/// <summary>
/// 文件分段,多段输出
/// </summary>
public class MultipartFileSender
{
protected log4net.ILog logger = log4net.LogManager.GetLogger(typeof(MultipartFileSender));
private const int DEFAULT_BUFFER_SIZE = 20480; // ..bytes = 20KB.
private const string MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES";
string filepath;
readonly HttpRequestBase request;
private HttpResponseBase response;
/// <summary>
/// 初始化
/// </summary>
/// <param name="path">文件物理路径</param>
public MultipartFileSender(string path, HttpRequestBase req, HttpResponseBase resp)
{
filepath = path;
request = req;
response = resp;
}
/// <summary>
/// 输出
/// </summary>
public void ServeResource()
{
if (response == null || request == null)
{
return;
}
if (filepath.IsNullOrWiteSpace() || !File.Exists(filepath))
{
logger.Error($"请求文件不存在 : {filepath}");
response.StatusCode = 404;
response.Flush();
response.Close();
return;
}
FileInfo fileinfo = new FileInfo(filepath);
long length = fileinfo.Length;
string fileName = Path.GetFileName(filepath);
string etag = fileName.GetMd5_16();
DateTime lastModifiedTime = fileinfo.LastWriteTimeUtc;
long lastModified = (long)(lastModifiedTime - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalSeconds;
string contentType = MimeMapping.GetMimeMapping(fileName);
// Validate request headers for caching ---------------------------------------------------
// If-None-Match header should contain "*" or ETag. If so, then return 304.
string ifNoneMatch = request.Headers["If-None-Match"];
if (ifNoneMatch != null && HttpMatches(ifNoneMatch, etag))
{
response.AddHeader("ETag", etag); // Required in 304.
response.StatusCode = 304;
response.Flush();
response.Close();
return;
}
// If-Modified-Since header should be greater than LastModified. If so, then return 304.
// This header is ignored if any If-None-Match header is specified.
long ifModifiedSince = request.Headers["If-Modified-Since"]?.ToLong() ?? -1;
if (ifNoneMatch == null && ifModifiedSince != -1 && ifModifiedSince + 1000 > lastModified)
{
response.AddHeader("ETag", etag); // Required in 304.
response.StatusCode = 304;
response.Flush();
response.Close();
return;
}
// Validate request headers for resume ----------------------------------------------------
// If-Match header should contain "*" or ETag. If not, then return 412.
string ifMatch = request.Headers["If-Match"];
if (ifMatch != null && !HttpMatches(ifMatch, etag))
{
response.StatusCode = 412;
response.Flush();
response.Close();
return;
}
// If-Unmodified-Since header should be greater than LastModified. If not, then return 412.
long ifUnmodifiedSince = request.Headers["If-Unmodified-Since"]?.ToLong() ?? -1;
if (ifUnmodifiedSince != -1 && ifUnmodifiedSince + 1000 <= lastModified)
{
response.StatusCode = 412;
response.Flush();
response.Close();
return;
}
// Validate and process range -------------------------------------------------------------
// Prepare some variables. The full Range represents the complete file.
HttpRange full = new HttpRange(0, length - 1, length);
List<HttpRange> ranges = new List<HttpRange>();
// Validate and process Range and If-Range headers.
string range = request.Headers["Range"];
if (range != null)
{
// Range header should match format "bytes=n-n,n-n,n-n...". If not, then return 416.
if (!Regex.IsMatch(range, "^bytes=\\d*-\\d*(,\\d*-\\d*)*$"))
{
response.AddHeader("Content-Range", "bytes */" + length); // Required in 416.
response.StatusCode = 416;
response.Flush();
response.Close();
return;
}
string ifRange = request.Headers["If-Range"];
if (ifRange != null && ifRange != etag)
{
try
{
long ifRangeTime = ifRange.ToLong(); // Throws IAE if invalid.
if (ifRangeTime != -1)
{
ranges.Add(full);
}
}
catch (Exception ignore)
{
ranges.Add(full);
}
}
// If any valid If-Range header, then process each part of byte range.
if (ranges.Count == 0)
{
foreach (var part in range.Substring(6).Split(','))
{
// Assuming a file with length of 100, the following examples returns bytes at:
// 50-80 (50 to 80), 40- (40 to length=100), -20 (length-20=80 to length=100).
long start = HttpRange.Sublong(part, 0, part.IndexOf("-", StringComparison.Ordinal));
long end = HttpRange.Sublong(part, part.IndexOf("-", StringComparison.Ordinal) + 1, part.Length);
if (start == -1)
{
start = length - end;
end = length - 1;
}
else if (end == -1 || end > length - 1)
{
end = length - 1;
}
// Check if Range is syntactically valid. If not, then return 416.
if (start > end)
{
response.AddHeader("Content-Range", "bytes */" + length); // Required in 416.
response.StatusCode = 416;
response.Flush();
response.Close();
return;
}
// Add range.
ranges.Add(new HttpRange(start, end, length));
}
}
}
// Prepare and initialize response --------------------------------------------------------
// Get content type by file name and set content disposition.
string disposition = "inline";
// If content type is unknown, then set the default value.
// For all content types, see: http://www.w3schools.com/media/media_mimeref.asp
// To add new content types, add new mime-mapping entry in web.xml.
if (contentType == null)
{
contentType = "application/octet-stream";
}
else if (!contentType.StartsWith("image"))
{
// Else, expect for images, determine content disposition. If content type is supported by
// the browser, then set to inline, else attachment which will pop a 'save as' dialogue.
string accept = request.Headers["Accept"];
disposition = accept != null && HttpAccepts(accept, contentType) ? "inline" : "attachment";
}
// Initialize response.
response.Clear();
response.ClearContent();
response.ClearHeaders();
response.Buffer = true;
response.AddHeader("Content-Type", contentType);
response.AddHeader("Content-Disposition", disposition + ";filename=\"" + fileName + "\"");
response.AddHeader("Accept-Ranges", "bytes");
response.AddHeader("ETag", etag);
response.Cache.SetLastModified(lastModifiedTime);
response.Cache.SetExpires(DateTime.UtcNow.AddDays(7));
// Send requested file (part(s)) to client ------------------------------------------------
// Prepare streams.
using (FileStream input = new FileStream(filepath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (Stream output = response.OutputStream)
{
if (ranges.Count == 0 || ranges[0] == full)
{
if (logger.IsDebugEnabled)
logger.Info($"Return full file,{filepath}");
response.ContentType = contentType;
response.AddHeader("Content-Range", "bytes " + full.start + "-" + full.end + "/" + full.total);
response.AddHeader("Content-Length", full.length.ToString());
HttpRange.Copy(input, output, length, full.start, full.length);
}
else if (ranges.Count == 1)
{
// Return single part of file.
HttpRange r = ranges[0];
logger.Info($"Return 1 part of file :{fileName}, from ({r.start}) to ({r.end})");
response.ContentType=contentType;
response.AddHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total);
response.AddHeader("Content-Length", r.length.ToString());
response.StatusCode = 206;
// Copy single part range.
HttpRange.Copy(input, output, length, r.start, r.length);
}
else
{
// Return multiple parts of file.
response.ContentType="multipart/byteranges; boundary=" + MULTIPART_BOUNDARY;
response.StatusCode = 206;
// Cast back to ServletOutputStream to get the easy println methods.
// Copy multi part range.
foreach (HttpRange r in ranges)
{
if (logger.IsDebugEnabled)
logger.Info($"Return multi part of file :{fileName} from ({r.start}) to ({r.end})");
// Copy single part range of multi part range.
HttpRange.Copy(input, output, length, r.start, r.length);
}
}
}
}
/**
* Returns true if the given accept header accepts the given value.
* @param acceptHeader The accept header.
* @param toAccept The value to be accepted.
* @return True if the given accept header accepts the given value.
*/
public static bool HttpAccepts(string acceptHeader, string toAccept)
{
string[] acceptValues =
acceptHeader.Split(new char[] { ',', '|', ';' }, StringSplitOptions.RemoveEmptyEntries);
return acceptValues.Contains(toAccept)
|| acceptValues.Contains(toAccept.Replace("/.*$", "/*"))
|| acceptValues.Contains("*/*");
}
/**
* Returns true if the given match header matches the given value.
* @param matchHeader The match header.
* @param toMatch The value to be matched.
* @return True if the given match header matches the given value.
*/
public static bool HttpMatches(string matchHeader, string toMatch)
{
string[] matchValues = matchHeader.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
return matchValues.Contains(toMatch)
|| matchValues.Contains("*");
}
class HttpRange
{
public long start;
public long end;
public long length;
public long total;
/**
* Construct a byte range.
* @param start Start of the byte range.
* @param end End of the byte range.
* @param total Total length of the byte source.
*/
public HttpRange(long start, long end, long total)
{
this.start = start;
this.end = end;
this.length = end - start + 1;
this.total = total;
}
public static long Sublong(string value, int beginIndex, int endIndex)
{
string substring = value.Substring(beginIndex, endIndex - beginIndex);
return (substring.Length > 0) ? substring.ToLong() : -1;
}
public static void Copy(Stream input, Stream output, long inputSize, long start, long length)
{
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int read;
if (inputSize == length)
{
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
output.Write(buffer, 0, read);
output.Flush();
}
}
else
{
input.Seek(start, SeekOrigin.Begin);
long toRead = length;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
if ((toRead -= read) > 0)
{
output.Write(buffer, 0, read);
output.Flush();
}
else
{
output.Write(buffer, 0, (int)toRead + read);
output.Flush();
break;
}
}
}
}
}
}
上一篇:C#经验总结