Search
Wednesday, February 27, 2002
 
  Home
  Archive
  Career
  Forums
  Feedback
  Bookmark Us
  Tell a Friend
  Newsletter
  Partners
   
 

 

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.

 
What did you think of this article?
  Not useful       Very useful  
Poorly written       Well written  
  Too short       Too long  

 

 

 

C# and the .NET Platform
Buy this book

 

C# Programming with the Public Beta
Buy this book

DevASP.Com

4 Guys From Rolla

Doug Dean Software

ASP Webring Member ASP WebringPrevious SiteNext Site
 
 

XML Applications
Buy this book

 

Database Programming with Visual Basic.NET
Buy this book

 

 

   
Copyright © 1998 - 2001 aspZone.com. All rights reserved. Terms of use.