ASP.NET 文件直接上传到磁盘并支持上传进度






4.77/5 (14投票s)
此模块为开发人员提供了一种方式来拦截 ASP.NET 中的多部分表单 POST 请求,并将数据直接存储到磁盘,而无需直接写入内存。它还提供了一种从外部窗口提取文件进度的方法。后端使用 C# 编写,进度条使用 JavaScript / Atlas。
引言
ASP.NET 是一种非常优秀且快速的开发小型到企业级网站的方式。大多数控件对于任何 Web 开发需求都非常完美;然而,FileUpload
控件确实存在一些不足。在我使用 .NET 1.1 时,我进行了一些测试,以了解该控件如何处理大文件,结果非常不令人满意。
我尝试上传一个 1 到 2GB 的超大文件。结果是我的网页报错,并且内存从未被释放。我没有继续研究,也没有再碰它。我确信 2.0 框架会修复这个问题,但看起来我们运气不好(至少我没找到)。
无论如何,我正在进行的项目要求我能够控制用户操作。你永远不知道何时会有人故意或意外地尝试上传一个巨大的文件。
目的
此模块旨在提供一种受控的文件处理方式,并允许最终用户查看其上传的当前进度。
代码示例
以下是此模块的主要源代码示例。如果您已经在网上搜索了关于这个主题的所有内容,那么您应该会看到一些相似之处;然而,我发现了一个很大的不同点,而其他开发者似乎没有这样做。让我解释一下。
许多关于如何做到这一点的示例都显示开发人员将字节数据转换为字符串,然后搜索字符串数据。这效率非常低下。大多数人创建这个模块是为了处理大文件下载。大多数人要 POST 的数据是二进制的。为什么要让 CPU 通过转换为字符串然后再搜索字符串来增加负担?我不知道,但也许当时听起来更容易。我的做法是,我在每个字节缓冲区中搜索字节模式。
另一个好处是,您的文件分隔符 **可能** 被分割到多个字节缓冲区中。在我看到的许多示例中,人们只查看一次缓冲区,然后就丢弃了。我将最后一个缓冲区保留在内存中以供参考,因此如果我在当前缓冲区中没有找到文件的开头,那么我将合并字节数组,然后重新搜索。
我不想说我的方法是最好的,但我认为只需对现有示例进行一些改进,它们就可以投入使用了。我测试过的许多示例,数据传输速度约为每秒 10 - 40 KB。是的……我需要等待 100 - 2000MB 的文件以这种速度上传吗?使用我的模块,我能够以每秒 1 - 4MB 的速度传输数据,并且 CPU 占用率很低。
特点
- 支持直接上传到磁盘
- 文件进度条(示例中使用 AJAX)
- 在配置的时间段后自动删除文件
- 快速缓冲区解析,从而降低 CPU 需求
- 可在 app.config 中配置
工作原理
上传模块实现了 IHttpModule
接口,并挂载到 context.BeginRequest
事件。当该事件触发时,我检查请求的页面是否是可能上传文件的页面(在 web.config 中配置)。我创建我的 FileProcessor
对象,然后开始从工作对象获取缓冲数据。我的代码注释将解释整个过程。
UploadModule.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Reflection;
using System.IO;
using System.Diagnostics;
using System.Threading;
namespace TravisWhidden
{
/// <summary>
/// This module will provide alternate
/// multi-part form posts abilities and also allow
/// the developer to display current progress for the file being uploaded.
/// </summary>
public class UploadModule : System.Web.IHttpModule
{
#region "ClassLevelVars"
/// <summary>
/// This is the base path where all the files will be uploaded.
/// This is set in the constructor.
/// Its value can be set by its default (built into the app),
/// or it can be set in the web.config of the application
/// </summary>
private string _baseFileLocation = "";
#endregion
#region "Constructors"
/// <summary>
/// Default Constructor
/// </summary>
public UploadModule()
{
_baseFileLocation =
TravisWhidden.Properties.Settings.Default.BaseFileUpload;
}
#endregion
#region "Properties"
/// <summary>
/// Used so we can read the base path from the config outside this assembly.
/// </summary>
public static string BasePath
{
get { return TravisWhidden.Properties.Settings.Default.BaseFileUpload; }
}
#endregion
#region "Methods"
/// <summary>
/// Method checks to see if the page that is being requested
/// is a page that this module will execute for.
/// </summary>
/// <returns>Returns true if this page was
/// listed in the web.config</returns>
private bool IsUploadPages()
{
HttpApplication app = HttpContext.Current.ApplicationInstance;
string[] uploadPages = (string[])
TravisWhidden.Properties.Settings.Default.Pages.Split(
new string[] {";"}, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < uploadPages.Length; i++)
{
if (uploadPages[i].ToLower() ==
app.Request.Path.Substring(1).ToLower())
return true;
}
return false;
}
#endregion
#region IHttpModule Members
/// <summary>
/// Cleanup interace object
/// </summary>
void System.Web.IHttpModule.Dispose()
{
}
/// <summary>
/// Interface method for Init
/// </summary>
/// <param name="context"></param>
void System.Web.IHttpModule.Init(System.Web.HttpApplication context)
{
// Attach handler to BeginRequest so we can process the messages
context.BeginRequest += new EventHandler(context_BeginRequest);
}
#endregion
#region "Event Handlers / Events"
/// <summary>
/// Method handler used to process the byte data being pushed in.
/// </summary>
/// <param name="sender">HttpApplication</param>
/// <param name="e">EventArgs</param>
void context_BeginRequest(object sender, EventArgs e)
{
// Get the HttpApplication object
HttpApplication httpApplication = (HttpApplication)sender;
// Get the current Context object
HttpContext context = httpApplication.Context;
// Create an instance of the file processor
// before we go any further, lets make sure we are to even watch this page
if (!IsUploadPages())
{
// Exit the method.
Debug.WriteLine("UploadModule - Not IsUploadPages().");
return;
}
Debug.WriteLine("UploadModule - IsUploadPages().");
// Create the file processor object
// with the base path in its constructor. this object
// will process all the byte data that we recive through the form post.
FileProcessor fp = new FileProcessor(_baseFileLocation);
// Get the current URL
string rawURL = context.Request.RawUrl;
// UniqueIdentifier to represent this post.
Guid currentFileID = Guid.NewGuid();
// the UploadStatus object is stored
// in the Application memory that is global to the application.
// this is what we will use to track the upload process
UploadStatus uploadStatus = new UploadStatus(currentFileID);
// Make sure we have an identifier. We use a post
// identifier so we can track outside our upload the
// upload status. If it does not have postID,
// we will add one to the current URL, and redirect the form
// to itself with one in it.
if (context.Request.QueryString["PostID"] != null &&
context.Request.QueryString["PostID"].Length ==
Guid.NewGuid().ToString().Length)
{
currentFileID = new Guid(context.Request.QueryString["PostID"]);
}
else
{
// Redirect to this same page, but with a PostID
if (rawURL.IndexOf("?") == -1)
{
rawURL = rawURL + "?PostID=" + currentFileID.ToString();
}
else
{
rawURL = rawURL + "&PostID=" + currentFileID.ToString();
}
context.Response.Redirect(rawURL);
}
// Make sure this is a multi-part form. If not, exit
if (context.Request.ContentType.IndexOf("multipart/form-data") == -1)
{
// Not multi-part form
return;
}
Debug.WriteLine("UploadModule Executing.");
// Get the worker request object. This object provides us with the byte data
// as the user is uploading it. This is a very critical part of this module.
HttpWorkerRequest workerRequest =
(HttpWorkerRequest)context.GetType().GetProperty(
"WorkerRequest", BindingFlags.Instance |
BindingFlags.NonPublic).GetValue(context, null);
// Indicates if the worker request has a body
if (workerRequest.HasEntityBody())
{
try
{
// Add the upload status to the appliation object.
context.Application.Add(currentFileID.ToString(), uploadStatus);
// Get the byte size of the form post. We need this
// to detect how much we have left to go.
long contentLength = long.Parse((workerRequest.GetKnownRequestHeader(
HttpWorkerRequest.HeaderContentLength)));
// the upload status object sets the length of the object.
// This is used for progress tracking outside this module.
uploadStatus.TotalBytes = contentLength;
long receivedcount = 0;
// this is the default buffer size. It appears that the
// most you can get out of the worker request object
// so we might as well just use these chunks.
long defaultBuffer = 8192;
// Get the preloaded buffer data. I have seen
// some problems with this running in the
// Visual Studios 2005 built in webserver.
// Sometimes it will return null, but I have not seen this happen on
// my windows 2000 or 2003 server boxes. I researched this problem
// and could not find any solution to the built in VS webserver,
// but that does not matter because that is just for testing
// anyways. You could always user your local IIS to test this module.
byte[] preloadedBufferData = workerRequest.GetPreloadedEntityBody();
// Just a check to throw an error that is understandable
if (preloadedBufferData == null)
{
throw new Exception("GetPreloadedEntityBody() " +
"was null. Try again");
}
// Extract header information needed.
fp.GetFieldSeperators(ref preloadedBufferData);
// Process this buffer.
fp.ProcessBuffer(ref preloadedBufferData, true);
// Update the status object.
uploadStatus.CurrentBytesTransfered += preloadedBufferData.Length;
// It is possible for all the data that was in the form post to be
// inside one buffer. if that is true,
// then the IsEntireEntityBodyIsPreloaded()
// will return true. There is no reason to continue on.
if (!workerRequest.IsEntireEntityBodyIsPreloaded())
{
// Data is not all preloaded.
do
{
// Because of a problem where we will
// be waiting for the ReadEntityBody to end
// when there is nothing left in the
// buffer, we have to only fill what is
// needed to finish the buffer.
// When the last buffer comes up, we have to resize
// the buffer to finish the rest
// of the array. This will allow us to exit
// this loop faster. If we tried to fill
// the buffer when there was nothing left to be
// filled, it would just hang till what I assumed
// timed out waiting for more buffer data internally.
long tempBufferSize = (uploadStatus.TotalBytes -
uploadStatus.CurrentBytesTransfered);
if (tempBufferSize < defaultBuffer)
{
defaultBuffer = tempBufferSize;
}
// Create the new byte buffer with the default size.
byte[] bufferData = new byte[defaultBuffer];
// Ask the worker request for the buffer chunk.
receivedcount =
workerRequest.ReadEntityBody(bufferData, bufferData.Length);
// Process the buffered data.
fp.ProcessBuffer(ref bufferData, true);
// Update the status object.
uploadStatus.CurrentBytesTransfered += bufferData.Length;
} while (receivedcount != 0);
}
}
catch (Exception ex)
{
Debug.WriteLine("Error: " + ex.Message);
Debug.WriteLine(ex.ToString());
}
finally
{
// makes sure that any open streams are closed for safety
fp.CloseStreams();
}
// join the array of finished files.
string finishedFiles = string.Join(";",
fp.FinishedFiles.ToArray());
// Add to query string.
if (rawURL.IndexOf("?") == -1 &&
finishedFiles.Length > 0)
{
rawURL = rawURL + "?finishedFiles=" + finishedFiles;
}else{
rawURL = rawURL + "&finishedFiles=" + finishedFiles;
}
// dispose the FileProcessor object.
fp.Dispose();
// redirect the user back to the page it was
// posted on, but with the new URL variables.
context.Response.Redirect(rawURL);
}
}
#endregion
}
}
FileProcessor.cs
FileProcessor
类将处理来自 WorkerRequest
对象传入的缓冲区。它负责在表单 POST 中查找文件,提取数据,并跟踪它提取的文件。
using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Text;
using System.IO;
using System.Threading;
namespace TravisWhidden
{
/// <summary>
/// FileProcessor is used to process byte data from
/// a multi-part form and save it to the disk.
/// </summary>
public class FileProcessor : IDisposable
{
#region "Class Vars"
/// <summary>
/// Form post id is used in finding field seperators in the multi-part form post
/// </summary>
private string _formPostID = "";
/// <summary>
/// Used to find the start of a file
/// </summary>
private string _fieldSeperator = "";
/// <summary>
/// Used to note what buffer index we are on in the collection
/// </summary>
private long _currentBufferIndex = 0;
/// <summary>
/// Used to flag each byte process if we have
/// already found the start of a file or not
/// </summary>
private bool _startFound = false;
/// <summary>
/// Used to flag each byte process if we have already found the end of a file.
/// </summary>
private bool _endFound = false;
/// <summary>
/// Default string to where the files are to be uploaded.
/// It will be overrided on the constructor
/// </summary>
private string _currentFilePath = @"C:\upload\";
/// <summary>
/// The way I wrote this is that I did not care
/// about the file name in the form. I generate the filename
/// to prevent anyone from posting two of the same
/// files at the same time. Each file is unique
/// and the form is redirected with the filenames
/// as URL params so the .net application can handle
/// the finished files as it wants.
/// </summary>
private string _currentFileName = Guid.NewGuid().ToString() + ".bin";
/// <summary>
/// FileStream object that is left open while a file
/// is beting written to. Each file will open and
/// close its filestream automaticly
/// </summary>
private FileStream _fileStream = null;
/// <summary>
/// The following fields are used in the byte searching of buffer datas
/// </summary>
private long _startIndexBufferID = -1;
private int _startLocationInBufferID = -1;
private long _endIndexBufferID = -1;
private int _endLocationInBufferID = -1;
/// <summary>
/// Dictionary array used to store byte chunks.
/// Should store a max of 2 items. 2 history chunks are kept.
/// </summary>
private Dictionary<long, byte[]> _bufferHistory =
new Dictionary<long, byte[]>();
/// <summary>
/// Object to hold all the finished filenames.
/// </summary>
private List<string> _finishedFiles = new List<string>();
#endregion
#region "Constructors"
/// <summary>
/// Default constructor with the path of where the files should be uploaded.
/// </summary>
public FileProcessor(string uploadLocation)
{
// This is the path where the files will be uploaded too.
_currentFilePath = uploadLocation;
}
#endregion
#region "Properties"
/// <summary>
/// Property used to get the finished files.
/// </summary>
public List<string> FinishedFiles
{
get { return _finishedFiles; }
}
#endregion
#region "Methods"
/// <summary>
/// Method that is used to process the buffer. It may call itself several times
/// as it looks for new files that may be possible in each buffer.
/// </summary>
/// <param name="bufferData">Byte data to scan</param>
/// <param name="addToBufferHistory">If true,
/// it will add it to the buffer history. If false, it will not.</param>
public void ProcessBuffer(ref byte[] bufferData, bool addToBufferHistory)
{
int byteLocation = -1;
// If the start has not been found, search for it.
if (!_startFound)
{
// Search for start location
byteLocation = GetStartBytePosition(ref bufferData);
if (byteLocation != -1)
{
// Set the start position to this current index
_startIndexBufferID = _currentBufferIndex + 1;
// Set the start location in the index
_startLocationInBufferID = byteLocation;
_startFound = true;
}
}
// If the start was found, we can start to store the data into a file.
if (_startFound)
{
// Save this data to a file
// Have to find the end point.
// Makes sure the end is not in the same buffer
int startLocation = 0;
if (byteLocation != -1)
{
startLocation = byteLocation;
}
// Write the data from the start point to the end point to the file.
int writeBytes = ( bufferData.Length - startLocation );
int tempEndByte = GetEndBytePosition(ref bufferData);
if (tempEndByte != -1)
{
writeBytes = (tempEndByte - startLocation);
// not that the end was found.
_endFound = true;
// Set the current index the file was found
_endIndexBufferID = (_currentBufferIndex + 1);
// Set the current byte location
// for the assoicated index the file was found
_endLocationInBufferID = tempEndByte;
}
// Make sure we have something to write.
if (writeBytes > 0)
{
if (_fileStream == null)
{
// create a new file stream to be used.
_fileStream = new FileStream(_currentFilePath +
_currentFileName, FileMode.OpenOrCreate);
// this will create a time to live for the
// file so it will automaticly be removed
int fileTimeToLive =
global::TravisWhidden.Properties.Settings.Default.FileTimeToLive;
// if the form were not to handle the file and remove
// it, this is an automatic removal of the file
// the timer object will execute in x number
// of seconds (can override in the web.config file)
Timer t = new Timer(new TimerCallback(DeleteFile),
_currentFilePath + _currentFileName,
(fileTimeToLive * 1000), 0);
}
// Write the datat to the file and flush it.
_fileStream.Write(bufferData, startLocation, writeBytes);
_fileStream.Flush();
}
}
// If the end was found, then we need
// to close the stream now that we are done with it.
// We will also re-process this buffer
// to make sure the start of another file
// is not located within it.
if (_endFound)
{
CloseStreams();
_startFound = false;
_endFound = false;
// Research the current buffer for a new start location.
ProcessBuffer(ref bufferData, false);
}
// Add to buffer history
if (addToBufferHistory)
{
// Add to history object.
_bufferHistory.Add(_currentBufferIndex, bufferData);
_currentBufferIndex++;
// Cleanup old buffer references
RemoveOldBufferData();
}
}
/// <summary>
/// Method used to clean up the internal buffer array.
/// Older elements are not needed, only a history of one is needed.
/// </summary>
private void RemoveOldBufferData()
{
for (long bufferIndex = _currentBufferIndex;
bufferIndex >= 0; bufferIndex--)
{
if (bufferIndex > (_currentBufferIndex - 3))
{
// Dont touch. preserving the last 2 items.
}
else
{
if (_bufferHistory.ContainsKey(bufferIndex))
{
_bufferHistory.Remove(bufferIndex);
}
else
{
// no more previous buffers.
bufferIndex = 0;
}
}
}
GC.Collect();
}
/// <summary>
/// Close the stream, and reset the filename.
/// </summary>
public void CloseStreams()
{
if (_fileStream != null)
{
_fileStream.Dispose();
_fileStream.Close();
_fileStream = null;
// add the file name to the finished list.
_finishedFiles.Add(_currentFileName);
// Reset the filename.
_currentFileName = Guid.NewGuid().ToString() + ".bin";
}
}
/// <summary>
/// This method should be ran on the bytes
/// that are returned on GetPreloadedEntityBody().
/// </summary>
/// <param name="bufferData"></param>
public void GetFieldSeperators(ref byte[] bufferData)
{
try
{
_formPostID = Encoding.UTF8.GetString(bufferData).Substring(29, 13);
_fieldSeperator =
"-----------------------------" + _formPostID;
}
catch (Exception ex)
{
Debug.WriteLine("Error in GetFieldSeperators(): " + ex.Message);
}
}
/// <summary>
/// Method used for searching buffer data, and if needed previous buffer data.
/// </summary>
/// <param name="bufferData">current
/// buffer data needing to be processed.</param>
/// <returns>Returns byte location of data to start</returns>
private int GetStartBytePosition(ref byte[] bufferData)
{
int byteOffset = 0;
// Check to see if the current bufferIndex
// is the same as any previous index found.
// If it is, offset the searching by the previous location
if (_startIndexBufferID == (_currentBufferIndex + 1))
{
byteOffset = _startLocationInBufferID;
}
// Check to see if the end index was found before
// this start index. That way we keep moving ahead
if (_endIndexBufferID == (_currentBufferIndex +1))
{
byteOffset = _endLocationInBufferID;
}
int tempContentTypeStart = -1;
// First see if we can find it in the current buffer batch.
// Because there may be muliple posts
// in a form, we have to make sure we do not
// re-return any possible byte offsets
// that we have returned before. This could lead
// to an infinite loop.
byte[] searchString = Encoding.UTF8.GetBytes("Content-Type: ");
tempContentTypeStart =
FindBytePattern(ref bufferData, ref searchString, byteOffset);
if (tempContentTypeStart != -1)
{
// Found content type start location
// Next search for \r\n\r\n at this substring
//int tempSearchStringLocation =
// bufferDataUTF8.IndexOf("\r\n\r\n", tempContentTypeStart);
searchString = Encoding.UTF8.GetBytes("\r\n\r\n");
int tempSearchStringLocation = FindBytePattern(ref bufferData,
ref searchString, tempContentTypeStart);
if (tempSearchStringLocation != -1)
{
// Found this. Can get start of data here
// Add 4 to it. That is the number of positions
// before it gets to the start of the data
int byteStart = tempSearchStringLocation + 4;
return byteStart;
}
}
else if((byteOffset - searchString.Length) > 0 ){
return -1;
}
else
{
// Did not find it. Add this and previous
// buffer together to see if it exists.
// Check to see if the buffer index is at the start.
if (_currentBufferIndex > 0)
{
// Get the previous buffer
byte[] previousBuffer = _bufferHistory[_currentBufferIndex - 1];
byte[] mergedBytes = MergeArrays(ref previousBuffer, ref bufferData);
// Get the byte array for the text
byte[] searchString2 = Encoding.UTF8.GetBytes("Content-Type: ");
// Search the bytes for the searchString
tempContentTypeStart = FindBytePattern(ref mergedBytes,
ref searchString2, previousBuffer.Length - searchString2.Length);
//tempContentTypeStart =
// combinedUTF8Data.IndexOf("Content-Type: ");
if (tempContentTypeStart != -1)
{
// Found content type start location
// Next search for \r\n\r\n at this substring
searchString2 = Encoding.UTF8.GetBytes("Content-Type: ");
// because we are searching part of the previosu buffer,
// we only need to go back the length of the search
// array. Any further, and our normal if statement
// would have picked it up when it first was processed.
int tempSearchStringLocation = FindBytePattern(ref mergedBytes,
ref searchString2, (previousBuffer.Length - searchString2.Length));
if (tempSearchStringLocation != -1)
{
// Found this. Can get start of data here
// It is possible for some of this
// to be located in the previous buffer.
// Find out where the excape chars are located.
if (tempSearchStringLocation > previousBuffer.Length)
{
// Located in the second object.
// If we used the previous buffer, we should
// not have to worry about going out of
// range unless the buffer was set to some
// really low number. So not going to check for
// out of range issues.
int currentBufferByteLocation =
(tempSearchStringLocation - previousBuffer.Length);
return currentBufferByteLocation;
}
else
{
// Located in the first object.
// The only reason this could happen is if
// the escape chars ended right
// at the end of the buffer. This would mean
// that that the next buffer would start data at offset 0
return 0;
}
}
}
}
}
// indicate not found.
return -1;
}
/// <summary>
/// Method used for searching buffer data for end
/// byte location, and if needed previous buffer data.
/// </summary>
/// <param name="bufferData">current
/// buffer data needing to be processed.</param>
/// <returns>Returns byte location of data to start</returns>
private int GetEndBytePosition(ref byte[] bufferData)
{
int byteOffset = 0;
// Check to see if the current bufferIndex
// is the same as any previous index found.
// If it is, offset the searching by the previous
// location. This will allow us to find the next leading
// Stop location so we do not return a byte offset
// that may have happened before the start index.
if (_startIndexBufferID == (_currentBufferIndex + 1))
{
byteOffset = _startLocationInBufferID;
}
int tempFieldSeperator = -1;
// First see if we can find it in the current buffer batch.
byte[] searchString = Encoding.UTF8.GetBytes(_fieldSeperator);
tempFieldSeperator = FindBytePattern(ref bufferData,
ref searchString, byteOffset);
if (tempFieldSeperator != -1)
{
// Found field ending. Depending on where the field
// seperator is located on this, we may have to move back into
// the prevoius buffer to return its offset.
if (tempFieldSeperator - 2 < 0)
{
//TODO: Have to get the previous buffer data.
}
else
{
return (tempFieldSeperator - 2);
}
}
else if ((byteOffset - searchString.Length) > 0)
{
return -1;
}
else
{
// Did not find it. Add this and
// previous buffer together to see if it exists.
// Check to see if the buffer index is at the start.
if (_currentBufferIndex > 0)
{
// Get the previous buffer
byte[] previousBuffer = _bufferHistory[_currentBufferIndex - 1];
byte[] mergedBytes = MergeArrays(ref previousBuffer, ref bufferData);
// Get the byte array for the text
byte[] searchString2 = Encoding.UTF8.GetBytes(_fieldSeperator);
// Search the bytes for the searchString
tempFieldSeperator = FindBytePattern(ref mergedBytes,
ref searchString2,
previousBuffer.Length - searchString2.Length + byteOffset);
if (tempFieldSeperator != -1)
{
// Found content type start location
// Next search for \r\n\r\n at this substring
searchString2 = Encoding.UTF8.GetBytes("\r\n\r\n");
int tempSearchStringLocation = FindBytePattern(ref mergedBytes,
ref searchString2, tempFieldSeperator);
if (tempSearchStringLocation != -1)
{
// Found this. Can get start of data here
// It is possible for some of this
// to be located in the previous buffer.
// Find out where the excape chars are located.
if (tempSearchStringLocation > previousBuffer.Length)
{
// Located in the second object.
// If we used the previous buffer,
// we shoudl not have to worry about going out of
// range unless the buffer was set to some
// really low number. So not going to check for
// out of range issues.
int currentBufferByteLocation =
(tempSearchStringLocation - previousBuffer.Length);
return currentBufferByteLocation;
}
else
{
// Located in the first object.
// The only reason this could happen is if
// the escape chars ended right
// at the end of the buffer. This would mean
// that that the next buffer would start data at offset 0
return -1;
}
}
}
}
}
// indicate not found.
return -1;
}
/// <summary>
/// Method created to search for byte array patterns inside a byte array.
/// </summary>
/// <param name="containerBytes">byte array to search</param>
/// <param name="searchBytes">byte
/// array with pattern to search with</param>
/// <param name="startAtIndex">byte offset
/// to start searching at a specified location</param>
/// <returns>-1 if not found or index
/// of starting location of pattern</returns>
private static int FindBytePattern(ref byte[] containerBytes,
ref byte[] searchBytes, int startAtIndex)
{
int returnValue = -1;
for (int byteIndex = startAtIndex; byteIndex <
containerBytes.Length; byteIndex++)
{
// Make sure the searchBytes lenght does not exceed the containerbytes
if (byteIndex + searchBytes.Length > containerBytes.Length)
{
// return -1.
return -1;
}
// First the first reference of the bytes to search
if (containerBytes == searchBytes[0])
{
bool found = true;
int tempStartIndex = byteIndex;
for (int searchBytesIndex = 1; searchBytesIndex <
searchBytes.Length; searchBytesIndex++)
{
// Set next index
tempStartIndex++;
if (!(searchBytes[searchBytesIndex] ==
containerBytes[tempStartIndex]))
{
found = false;
// break out of the loop and continue searching.
break;
}
}
if (found)
{
// Indicates that the byte array has been found. Return this index.
return byteIndex;
}
}
}
return returnValue;
}
/// <summary>
/// Used to merge two byte arrays into one.
/// </summary>
/// <param name="arrayOne">First byte array
/// to go to the start of the new array</param>
/// <param name="arrayTwo">Second byte array
/// to go to right after the first array that was passed</param>
/// <returns>new byte array of all the new bytes</returns>
private static byte[] MergeArrays(ref byte[] arrayOne, ref byte[] arrayTwo)
{
System.Type elementType = arrayOne.GetType().GetElementType();
byte[] newArray = new byte[arrayOne.Length + arrayTwo.Length];
arrayOne.CopyTo(newArray, 0);
arrayTwo.CopyTo(newArray, arrayOne.Length);
return newArray;
}
/// <summary>
/// This is used as part of the clean up procedures.
/// the Timer object will execute this function.
/// </summary>
/// <param name="filePath"></param>
private static void DeleteFile(object filePath)
{
// File may have already been removed from the main appliation.
try
{
if (System.IO.File.Exists((string)filePath))
{
System.IO.File.Delete((string)filePath);
}
}
catch { }
}
#endregion
#region IDisposable Members
/// <summary>
/// Clean up method.
/// </summary>
public void Dispose()
{
// Clear the buffer history
_bufferHistory.Clear();
GC.Collect();
}
#endregion
}
}
使用说明
每个页面都会看到这个模块;然而,我们不想使用它,除非它是文件上传页面。你会在 web.config 中看到一个部分,用于设置要监视的页面以及其他一些设置。
<applicationSettings>
<TravisWhidden.Properties.Settings>
<setting name="BaseFileUpload" serializeAs="String">
<value>C:\upload\</value>
</setting>
<setting name="FileTimeToLive" serializeAs="String">
<value>3600</value>
</setting>
<setting name="Pages" serializeAs="String">
<value>UploadModuleExample/Default.aspx;UploadModuleAtlasExample/
Default.aspx;/MembersOnly/ImageUploadManagement.aspx</value>
</setting>
</TravisWhidden.Properties.Settings>
</applicationSettings>
闭运算
此模块免费分发。如果您计划销售此模块,至少请将一部分收益寄给我。呵呵。
联系方式
如果您有任何问题或评论,可以通过 travis@lvfbody.com 给我的电子邮件发送信息。我很想听听我的模块是如何被使用的以及它的表现如何。
参考文献
- http://forums.asp.net/thread/106552.aspx
描述:关于如何实现此功能的非常有用的信息。其中一些代码示例效率极低,但能说明问题。
其他参考资料(基本上看起来像 ASP.NET 论坛上的内容)可以在 CodeProject.com 上找到。看到的一些示例并没有做到所有的事情。
附加信息
注意:在我撰写此文时,我发现了这个 MSDN 参考,我不知道这个项目是否可以做得更简单。希望我们没有重复造轮子。可能没什么,但值得一读:http://msdn2.microsoft.com/en-us/library/system.web.httppostedfile.aspx。
引起我注意的是:“文件以 MIME multipart/form-data 格式上传。默认情况下,所有大小超过 256 KB 的请求(包括表单字段和上传的文件)都会缓冲到磁盘,而不是保留在服务器内存中。”