|
ASP File Upload
using VBScript John R.
Lewis 7/10/2000
Contents
Introduction Support
Code Parsing
the Request Putting
it all Together Usage
Examples Conclusion
Source
Code
ASPUpload.zip
~ 3Kb
 |
| 836
users rated this article. |
| Usefulness |
|
4.3
out of 5 |
| Quality |
|
3.9
out of 5 |
| Length |
|
3.2
out of 5 | |
 |
| |
|
 |
 |
Introduction
There are several
components on the market that allow your ASP application to handle
file uploads. This article does not intend to replace those. They
perform quite well. There are times, however, that deploying a
custom COM object is not possible. For example, your ISP does not
allow them. This article will show you how to write a VBScript class
to handle file uploads.
Support Code
Before we go into the
details of parsing the request, we need to take care of some
supporting code. In particular, collections. We would like to expose
both the files, and other form elements as collections. This will
provide a very familiar interface, as collections are very common in
the ASP object model.
We could just use the
Scripting.Dictionary object as our collection, but it has a few
quirks that make its use a bit strange. For example, there is no way
to access a data element by its ordinal position, you can only
access it by its key.
We will wrap the functionality of the
dictionary in a VBScript class. We will loose the ability to use the
For..Each syntax to enumerate through the collection,
but it is worth it. The following code listing shows how to do it:
| Code Listing #1 |
Class clsCollection
'========================================================='
' This class is a pseudo-collection. It is not a real '
' collection, because there is no way that I am aware '
' of to implement an enumerator to support the '
' For..Each syntax using VBScript classes. '
'========================================================='
Private m_objDicItems
Private Sub Class_Initialize()
Set m_objDicItems =
Server.CreateObject("Scripting.Dictionary")
m_objDicItems.CompareMode = vbTextCompare
End Sub
Public Property Get Count()
Count = m_objDicItems.Count
End Property
Public Default Function Item(Index)
Dim arrItems
If IsNumeric(Index) Then
arrItems = m_objDicItems.Items
If IsObject(arrItems(Index)) Then
Set Item = arrItems(Index)
Else
Item = arrItems(Index)
End If
Else
If m_objDicItems.Exists(Index) Then
If IsObject(m_objDicItems.Item(Index)) Then
Set Item = m_objDicItems.Item(Index)
Else
Item = m_objDicItems.Item(Index)
End If
End If
End If
End Function
Public Function Key(Index)
Dim arrKeys
If IsNumeric(Index) Then
arrKeys = m_objDicItems
Key = arrKeys(Index)
End If
End Function
Public Sub Add(Name, Value)
If m_objDicItems.Exists(Name) Then
m_objDicItems.Item(Name) = Value
Else
m_objDicItems.Add Name, Value
End If
End Sub
End Class
|
Regular form elements
do not require any extra effort to store in a collection. They are
simply name/value pairs. Files, however, have additional properties
and methods. To store them in a collection, we will need some sort
of container. The following code provides a nice tidy way to do
this:
| Code Listing #2 |
Class clsFile
'========================================================='
' This class is used as a container for a file sent via '
' an http multipart/form-data post. '
'========================================================='
Private m_strName
Private m_strContentType
Private m_strFileName
Private m_Blob
Public Property Get Name()
Name = m_strName
End Property
Public Property Let Name(vIn)
m_strName = vIn
End Property
Public Property Get ContentType()
ContentType = m_strContentType
End Property
Public Property Let ContentType(vIn)
m_strContentType = vIn
End Property
Public Property Get FileName()
FileName = m_strFileName
End Property
Public Property Let FileName(vIn)
m_strFileName = vIn
End Property
Public Property Get Blob()
Blob = m_Blob
End Property
Public Property Let Blob(vIn)
m_Blob = vIn
End Property
Public Sub Save(Path)
Dim objFSO, objFSOFile
Dim lngLoop
Set objFSO = _
Server.CreateObject("Scripting.FileSystemObject")
Set objFSOFile = objFSO.CreateTextFile( _
objFSO.BuildPath(Path, m_strFileName))
For lngLoop = 1 to LenB(m_Blob)
objFSOFile.Write Chr(AscB(MidB(m_Blob, lngLoop, 1)))
Next
objFSOFile.Close
End Sub
End Class
|
The Request.Binary
read method is used to read the raw data sent by the client as part
of a POST request. It returns an array of unsigned one byte
characters. You cannot access it like a normal array because
VBScript only recognizes arrays of Variants. It does however allow
manipulation using the byte string manipulation methods AscB, ChrB,
RightB, LeftB, MidB, LenB, and InStrB.
But VBScript does not
provide us any way to convert from a Unicode string to a Byte string
and back. The following code uses the byte string manipulation
methods to do the conversion:
| Code Listing #3 |
Private Function BStr2UStr(BStr)
'Byte string to Unicode string conversion
Dim lngLoop
BStr2UStr = ""
For lngLoop = 1 to LenB(BStr)
BStr2UStr = BStr2UStr & Chr(AscB(MidB(BStr,lngLoop,1)))
Next
End Function
Private Function UStr2Bstr(UStr)
'Unicode string to Byte string conversion
Dim lngLoop
Dim strChar
UStr2Bstr = ""
For lngLoop = 1 to Len(UStr)
strChar = Mid(UStr, lngLoop, 1)
UStr2Bstr = UStr2Bstr & ChrB(AscB(strChar))
Next
End Function
|
Parsing the Request
To help in
understanding how to parse the request, consider the following test
page:
| Code Listing #4 |
<%@ Language=VBScript %>
<FORM method=post
ENCTYPE="multipart/form-data"
ACTION="test.asp">
Your Name:<BR>
<INPUT type="text" name="YourName">
<BR><BR>
Your File:<BR>
<INPUT type="file" name="YourFile">
<BR><BR>
<INPUT type="submit" name="submit" value="Upload">
</FORM>
<HR>
<PRE>
<%
If Request.TotalBytes > 0 Then
Response.BinaryWrite(Request.BinaryRead(Request.TotalBytes))
End If
%>
</PRE>
|
Which spits out
something like this:
-----------------------------7d02e839104d4
Content-Disposition: form-data; name="YourName"
John Lewis
-----------------------------7d02e839104d4
Content-Disposition: form-data; name="YourFile";
filename="C:\Documents and Settings\JohnL\Desktop\Test.zip"
Content-Type: application/x-zip-compressed
PK
oG樝ɃƏTest.txtThis is a test.PK
oG樝ɃƏ ¶Test.txtPK65
-----------------------------7d02e839104d4
Content-Disposition: form-data; name="submit"
Upload
-----------------------------7d02e839104d4--
|
Each form element is
delimited by a boundary:
-----------------------------7d02e839104d4 |
The final boundary is
followed by a '--':
-----------------------------7d02e839104d4-- |
Within each boundary
there is a Content-Disposition element with a semi-colon delimited
list of arguments. The name argument is the name of the form element
(i.e. YourName, YourFile, Submit). If the form element type is file,
there will be an additional argument, filename, which is the name of
the file uploaded, and there will be a Content-Type
element containing the mime-type of the file uploaded. The format is
not all that difficult to deal with. Just loop through the whole
mess, looking for the next boundary, and stop when you get to the
end.
But what if an HTML form without the proper ENCTYPE
posted to our upload code? We have no way of determining this prior
to calling Request.BinaryRead. Once we do this, we can no longer
access the Request.Form collection. We need to be able to handle
application/x-www-form-urlencoded as well.
Lets examine this
further. If we were to modify the code listing above by removing the
ENCTYPE="multipart/form-data", we would get output that
looks something like this:
YourName=John+Lewis&YourFile=C%3A%5CDocuments+and+Settings
%5CJohnL%5CDesktop%5CTest.zip&submit=Upload |
Which is an ampersand
(&) delimited list of form element name/value pairs. Spaces are
replaced with plus signs (+), and illegal characters are escaped
into their hexadecimal value preceded by a percent sign (%). We will
need to loop through, separate the individual name/value pairs, and
then decode them. ASP provides a method to apply URL encoding rules,
but no publicly exposed way to decode. So we need to implement one
ourselves:
| Code Listing #5 |
Private Function URLDecode(Expression)
'Why doesn't ASP provide this functionality for us?
Dim strSource, strTemp, strResult
Dim lngPos
strSource = Replace(Expression, "+", " ")
For lngPos = 1 To Len(strSource)
strTemp = Mid(strSource, lngPos, 1)
If strTemp = "%" Then
If lngPos + 2 < Len(strSource) Then
strResult = strResult & _
Chr(CInt("&H" & Mid(strSource, lngPos + 1, 2)))
lngPos = lngPos + 2
End If
Else
strResult = strResult & strTemp
End If
Next
URLDecode = strResult
End Function
|
So, with all that out
of the way, we can take a look at the code that actually parses the
request:
| Code Listing #6 |
Private Sub ParseRequest()
Dim lngTotalBytes, lngPosBeg, lngPosEnd
Dim lngPosBoundary, lngPosTmp, lngPosFileName
Dim strBRequest, strBBoundary, strBContent
Dim strName, strFileName, strContentType
Dim strValue, strTemp
Dim objFile
'Grab the entire contents of the Request as a Byte string
lngTotalBytes = Request.TotalBytes
strBRequest = Request.BinaryRead(lngTotalBytes)
'Find the first Boundary
lngPosBeg = 1
lngPosEnd = _
InStrB(lngPosBeg, strBRequest, UStr2Bstr(Chr(13)))
If lngPosEnd > 0 Then
strBBoundary = _
MidB(strBRequest, lngPosBeg, lngPosEnd - lngPosBeg)
lngPosBoundary = InStrB(1, strBRequest, strBBoundary)
End If
If strBBoundary = "" Then
'The form must have been submitted *without*
'ENCTYPE="multipart/form-data"
'But since we already called Request.BinaryRead,
'we can no longer access the Request.Form collection,
'so we need to parse the request and populate
'our own form collection.
lngPosBeg = 1
lngPosEnd = _
InStrB(lngPosBeg, strBRequest, UStr2BStr("&"))
Do While lngPosBeg < LenB(strBRequest)
'Parse the element and add it to the collection
strTemp = BStr2UStr(MidB(strBRequest, _
lngPosBeg, lngPosEnd - lngPosBeg))
lngPosTmp = InStr(1, strTemp, "=")
strName = URLDecode(Left(strTemp, lngPosTmp - 1))
strValue = URLDecode(Right(strTemp, _
Len(strTemp) - lngPosTmp))
m_objForm.Add strName, strValue
'Find the next element
lngPosBeg = lngPosEnd + 1
lngPosEnd = InStrB(lngPosBeg, _
strBRequest, UStr2BStr("&"))
If lngPosEnd = 0 Then
lngPosEnd = LenB(strBRequest) + 1
End If
Loop
Else
'Form was submitted with ENCTYPE="multipart/form-data"
'Loop through all the boundaries, and parse them
'into either the Form or Files collections.
Do Until (lngPosBoundary = _
InStrB(strBRequest, strBBoundary & UStr2Bstr("--")))
'Get the element name
lngPosTmp = InStrB(lngPosBoundary, strBRequest, _
UStr2BStr("Content-Disposition"))
lngPosTmp = InStrB(lngPosTmp, _
strBRequest, UStr2BStr("name="))
lngPosBeg = lngPosTmp + 6
lngPosEnd = InStrB(lngPosBeg, _
strBRequest, UStr2BStr(Chr(34)))
strName = BStr2UStr(MidB(strBRequest, _
lngPosBeg, lngPosEnd - lngPosBeg))
'Look for an element named 'filename'
lngPosFileName = InStrB(lngPosBoundary, _
strBRequest, UStr2BStr("filename="))
'If found, we have a file,
'otherwise it is a normal form element
If lngPosFileName <> 0 And lngPosFileName < _
InStrB(lngPosEnd, strBRequest, strBBoundary) Then
'It is a file. Get the FileName
lngPosBeg = lngPosFileName + 10
lngPosEnd = InStrB(lngPosBeg, _
strBRequest, UStr2BStr(chr(34)))
strFileName = BStr2UStr(MidB(strBRequest, _
lngPosBeg, lngPosEnd - lngPosBeg))
'Get the ContentType
lngPosTmp = InStrB(lngPosEnd, _
strBRequest, UStr2BStr("Content-Type:"))
lngPosBeg = lngPosTmp + 14
lngPosEnd = InstrB(lngPosBeg, _
strBRequest, UStr2BStr(chr(13)))
strContentType = BStr2UStr(MidB(strBRequest, _
lngPosBeg, lngPosEnd - lngPosBeg))
'Get the Content
lngPosBeg = lngPosEnd + 4
lngPosEnd = InStrB(lngPosBeg, _
strBRequest, strBBoundary) - 2
strBContent = MidB(strBRequest, _
lngPosBeg, lngPosEnd - lngPosBeg)
If strFileName <> "" And strBContent <> "" Then
'Create the File object,
'and add it to the Files collection
Set objFile = New clsFile
objFile.Name = strName
objFile.FileName = Right(strFileName, _
Len(strFileName) - InStrRev(strFileName, "\"))
objFile.ContentType = strContentType
objFile.Blob = strBContent
m_objFiles.Add strName, objFile
End If
Else 'It is a form element
'Get the value of the form element
lngPosTmp = InStrB(lngPosTmp, _
strBRequest, UStr2BStr(chr(13)))
lngPosBeg = lngPosTmp + 4
lngPosEnd = InStrB(lngPosBeg, _
strBRequest, strBBoundary) - 2
strValue = _
BStr2UStr(MidB(strBRequest, _
lngPosBeg, lngPosEnd - lngPosBeg))
'Add the element to the collection
m_objForm.Add strName, strValue
End If
'Move to Next Element
lngPosBoundary = InStrB(lngPosBoundary + _
LenB(strBBoundary), strBRequest, strBBoundary)
Loop
End If
End Sub
|
Putting it all Together
Our upload class will
expose two properties, Form and Files. These are both collections.
Each has very similar functionality to the Request.Form collection.
Here is the class declaration, I've left out the private functions:
| Code Listing #7 |
Class clsUpload
'========================================================='
' This class will parse the binary contents of the '
' request, and populate the Form and Files collections. '
'========================================================='
Private m_objFiles
Private m_objForm
Public Property Get Form()
Set Form = m_objForm
End Property
Public Property Get Files()
Set Files = m_objFiles
End Property
Private Sub Class_Initialize()
Set m_objFiles = New clsCollection
Set m_objForm = New clsCollection
ParseRequest
End Sub
...
End Class
|
Usage Examples
In this example, all
the upload code is placed in an include file.
| Code Listing #8 |
<%@ Language=VBScript %>
<!-- #include file="clsUpload.asp" -->
<form method=post
enctype="multipart/form-data"
action=test.asp id=form1 name=form1>
Your Name:<BR><input type=text name=YourName><BR><BR>
Your File:<BR><input type=file name=YourFile><BR><BR>
<input type=submit name=submit value="Upload">
</form>
<HR>
<%
Dim objUpload, lngLoop
If Request.TotalBytes > 0 Then
Set objUpload = New clsUpload
%>
File(s) Uploaded: <%= objUpload.Files.Count %>
<BR><BR>
<%
For lngLoop = 0 to objUpload.Files.Count - 1
'If accessing this page annonymously,
'the internet guest account must have
'write permission to the path below.
objUpload.Files.Item(lngLoop).Save "c:\uploads\"
%>
Form Element Name:
<%= objUpload.Files.Key(lngLoop) %>
<BR>
File Name:
<%= objUpload.Files.Item(lngLoop).FileName %>
<BR><BR>
<%
Next
%>
Other Form Element(s): <%= objUpload.Form.Count %>
<BR><BR>
<%
For lngLoop = 0 to objUpload.Form.Count - 1
%>
Form Element Name:
<%= objUpload.Form.Key(lngLoop) %>
<BR>
Form Element Value:
<%= objUpload.Form.Item(lngLoop) %>
<BR><BR>
<%
Next
End If
%>
|
Conclusion
So as you can see, it
is not all that difficult to handle file uploads using ASP and
VBScript. This example has been lightly tested using Windows 2000
Server and Internet Explorer 5. There may be bugs on other versions
of the server or client. This code is for demonstration purposes
only, and not for production use. Your milage may vary.
|