Introduction
File uploading through HTTP is always a big problem for websites. There are some restrictions from the client and server sides. But with growing internet channel bandwidths, one of the major problems is file size. Sometimes it's impossible to send a 500 MB file to a web server due to request length limits. One of the workarounds is to increase the maximal request length on the web server, but it may cause the server to restart when the memory limit is exceeded. For example: an IIS APS.NET web server. If we increase maxRequestLength
to 500 MB, the memoryLimit
default value is 60%; it means that the process will be recycled when it uses more than 60% of the physical memory. If we have 1 GB of physical memory in the system and a couple of users simultaneously upload 400 MB files, there is a high chance the web server will be restarted, because the server wouldn't have time to release memory from the Request
objects.
Another big issue is file upload continuing, when a process is interrupted by some reason. Normally, the user needs to upload the whole file once again.
In this example, I'll describe how to implement a file uploading method using AJAX and Web Service technologies. Of course, this method has its own restrictions, but it would be quite useful for intranet solutions and administrative areas in internet websites.
How it works
The main idea is quite simple. We should read a file partially and send these parts to the web server.
Client-side
function getFileParams()
{
this.filePath =
document.getElementById("file").value.replace(
/\\/g, "\\\\");
fso = new ActiveXObject( 'Scripting.FileSystemObject' );
if ( !fso.FileExists(this.filePath) )
{
alert("Can't open file.");
return;
}
f = fso.GetFile( this.filePath );
this.fileSize = f.size;
this.fileName = f.Name;
InitStatusForm();
InitUpload();
}
Allocate the file on the client and get the file size. I use the Scripting.FileSystemObject
ActiveX object to get the file size because this object will not load the full file in memory. Then, init the form layout and upload process using the InitStatusForm()
and InitUpload
functions.
function InitUpload()
{
document.getElementById("uploadConsole").style.display = "none";
document.getElementById("statusConsole").style.display = "block";
xmlhttp = new ActiveXObject( "Microsoft.XMLHTTP" );
xmlhttp.onreadystatechange = HandleStateChange;
var parameters = "fileSize=" + encodeURI(this.fileSize) +
"&fileName=" + encodeURI(this.fileName)+
"&overwriteFile=" +
encodeURI(document.getElementById("overwriteFile").checked);
xmlhttp.open("POST",
"http://localhost/AJAXUpload/Upload.asmx/InitUpload", true);
xmlhttp.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xmlhttp.setRequestHeader("Content-length", parameters.length);
xmlhttp.setRequestHeader("Connection", "close");
xmlhttp.send(parameters);
}
Init upload: Create the XmlHttp
object, and the send to the Web Service initial information such as file size, file name, and overwrite flag.
function HandleStateChange() {
switch (xmlhttp.readyState) {
case 4:
response = xmlhttp.responseXML.documentElement;
id = response.getElementsByTagName('ID')[0].firstChild.data;
offset = esponse.getElementsByTagName('OffSet')[0].firstChild.data;
bufferLength =
response.getElementsByTagName('BufferLength')[0].firstChild.data;
percentage = (offset/this.fileSize)*100;
if (offset<this.fileSize && !this.cancelUpload)
{
UpdateStatusConsole(percentage, "Uploading");
SendFilePart(offset, bufferLength);
}
else
{
SetButtonCloseState(false);
if (this.cancelUpload)
UpdateStatusConsole(percentage, "Canceled");
else
UpdateStatusConsole(percentage, "Complete");
}
break;
}
}
Asynchronous requests from the server-side is handled by the HandledStateChange()
callback function. Parse these parameters from the server:
id
- response-request identifier
offset
- start position to read the file part
bufferLength
- file block size to read
If requested, the start position should not exceed file size and upload should not be canceled by the user we send the file part to.
function SendFilePart(offset, length)
{
var xmlSOAP = new ActiveXObject("MSXML2.DOMDocument");
xmlSOAP.loadXML('<?xml version="1.0" encoding="utf-8"?>'+
'<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '+
'xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> '+
'<soap:Body>'+
'<UploadData xmlns="http://tempuri.org/" >'+
'<fileName>'+this.fileName+'</fileName>'+
'<fileSize>'+this.fileSize+'</fileSize>'+
'<file></file>'+
'</UploadData>'+
'</soap:Body>'+
'</soap:Envelope>');
var fileNode = xmlSOAP.selectSingleNode("//file");
fileNode.dataType = "bin.base64";
if (adoStream.State != 1 )
{
adoStream.Type = 1; adoStream.Open();
adoStream.LoadFromFile(this.filePath);
}
adoStream.Position = offset;
fileNode.nodeTypedValue = adoStream.Read(length);
if (adoStream.EOS)
{
adoStream.Close();
}
xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
xmlhttp.onreadystatechange = HandleStateChange;
xmlhttp.open("POST",
"http://localhost/AJAXUpload/Upload.asmx", true);
xmlhttp.setRequestHeader("SOAPAction",
"http://tempuri.org/UploadData");
xmlhttp.setRequestHeader("Content-Type",
"text/xml; charset=utf-8");
xmlhttp.send(xmlSOAP);
}
In this function, we create XmlSoap
, read the file part using the ADODB.Stream
ActiveX object, and send it to the web server with the file name and the file size. The sServer response from this operation would be handled by the same HandledStateChange()
callback function.
Server-side
[WebMethod]
public XmlDocument InitUpload(int fileSize, string fileName, bool overwriteFile )
{
long offset = 0;
string filePath = GetFilePath(fileName);
if (File.Exists(filePath))
{
if (overwriteFile)
{
File.Delete(filePath);
}
else
{
using (FileStream fs = File.Open(filePath, FileMode.Append))
{
offset = fs.Length;
}
}
}
return GetXmlDocument(Guid.NewGuid(), string.Empty, offset,
(InitialBufferLength+offset)>fileSize?
(int)(fileSize-offset):InitialBufferLength);
}
Init the upload server-side function. If a file with the same name already exists and the overwrite flag is false
, the existing file will be appended to; otherwise, the file will be deleted. Then, construct the response using the GetXmlDocument
function.
[WebMethod]
public XmlDocument UploadData(string fileName, int fileSize, byte[] file)
{
if (fileName == null || fileName == string.Empty || file == null)
return GetXmlDocument(Guid.NewGuid(),
"Incorrect UploadData Request", 0, 0);
string filePath = GetFilePath(fileName);
long offset=0;
using (FileStream fs = File.Open(filePath, FileMode.Append))
{
fs.Write(file, 0, file.Length);
offset = fs.Length;
}
return GetXmlDocument(Guid.NewGuid(), string.Empty, offset,
(InitialBufferLength+offset)>fileSize?
(int)(fileSize-offset):InitialBufferLength);
}
This method handles the request from the client with the file part data. Append the file part to the uploaded part and request the next part.
Install and run
To run the project, you should do a couple manipulations:
- Give read/write permissions to your IIS user for the upload folder.
- Enable ActiveX objects in your IE browser. (Add the website to the trusted websites list.)
Remarks
I've described the solution core; all layout functionality like upload panel and progress bar can be found in the included project.
Please don't use this solution as is in your projects. It's just an AJAX upload form example. When working with streams, files, and ActiveX objects, we should handle all error cases.
Links