ASP无组件上传·从原理剖析到实践(中)

发表于:2007-06-30来源:作者:点击数: 标签:
第五天:得到文件单元 今天我们要进行的部分,是比较有趣味性地——得到文件内容。其实,看看我们的要处理的数据,再看看前天文本单元的处理,相信大家也会心中有数。 为了清晰的区分文件和文本单元,这一次,我们用ourRequest.file(index)来对应文本单元的ou
第五天:得到文件单元

今天我们要进行的部分,是比较有趣味性地——得到文件内容。其实,看看我们的要处理的数据,再看看前天文本单元的处理,相信大家也会心中有数。

为了清晰的区分文件和文本单元,这一次,我们用ourRequest.file(index)来对应文本单元的ourRequest.form(index)。当然,因为对于文件,我们需要得到的信息不同于文本,所以这次得到的,也不会是FormElement,而是一个新对象FileElement。

文件单元和文本单元在原始数据上,不同点少得可怜:
1。第一行多了一个filename="xxx"模块;
2。多了一个用于指示contentType的第二行。

感兴趣的目标信息不同,所以,得到的对象FileElement也和FormElement有一些不同点:
1。不需要count属性(不存在checkbox情况);
2。不需要Item(index)(同上,不存在checkbox情况);
3。需要一个ContentType属性;
4。需要一个FilePath属性;
5。需要一个FileName属性;
6。需要一个Size属性;
7。因为需要的是二进制,所以,没有必要进行二进制=>字符串的转换;
8。因为需要的是二进制,所以,属性Value改成Data更合适

此外,UploadRequest也应该相应的添加Files属性、Form(index)方法、以及m_dicFiles成员。现在,我们就来扩充他:
A。UploadRequest(上面设计过,这里是扩充)
这个类和request对象是对应的
属性:
RawData 得到原始数据,方便检查[只读]
Forms 得到一个有count属性的计数器,
可以用outRequest.Forms.Count的方式,得到文本表单域的的个数[只读]
Files 得到一个有count属性的计数器,
可以用outRequest.Files.Count的方式,得到文件表单域的的个数[只读]
Form(index) 可以用数字或文本检索文本表单域,做用类似request.form。
他返回一个FormElement型的对象
File(index) 可以用数字或文本检索文件表单域,他返回一个FileElement型的对象
B。FileElement
可以把它看成单个文件域的化身。通过这个类,可以得到详细的文件信息,比如name,data,path,filename,contentType,size等等。
属性:
Name 文件域的名称。就是<input type=file name=xxx>里的xxx
Data 文件域的内容。二进制串
ContentType 文件域的contentType
FilePath 文件域包含的文件在客户机上的全路径
FileName 文件域包含的文件的文件名
Size 文件域包含的文件的尺寸

这里是实现。还是存成doupload.asp:
<%
‘’=========================================================================
‘’‘’ 这个,是存储文本域信息的的类。每一个name的文本域,对应一个这样的类。
‘’=========================================================================
Class FormElement

‘’ m_开头,表示类成员变量。
Private m_dicItems

Private Sub Class_Initialize()
Set m_dicItems = Server.CreateObject("Scripting.Dictionary")
End Sub

‘’ count是咱们这个类的一个只读属性
Public Property Get Count()
Count = m_dicItems.Count
End Property

‘’ Value是一个默认属性。目的是得到值
Public Default Property Get Value()
Value = Item("")
End Property

‘’ Name是得到文本域名称。就是<input name=xxx>里的xxx
Public Property Get Name()
Keys = m_dicItems.Keys
Name = Keys(0)
Name = left(Name,instrrev(Name,"_")-1)
End Property

‘’ Item属性用来得到重名表单域(比如checkbox)的某一个值
Public Property Get Item(index)
If isNumeric(index) Then ‘’是数字,合法!
If index > m_dicItems.Count-1 Then
err.raise 1,"IndexOutOfBound", "表单元素子集索引越界"
End If
Itms = m_dicItems.Items
Item = Itms(index)
ElseIf index = "" Then ‘’没给值?那就返回所有的!逗号分隔
Itms = m_dicItems.Items
For i = 0 to m_dicItems.Count-1
If i = 0 Then
Item = Itms(0)
Else
Item = Item & "," & Itms(i)
End If
Next
Else ‘’给个一个不是数字的东东?出错!
err.raise 2,"IllegalArgument", "非法的表单元素子集索引"
End If
End Property

Public Sub Add(key, item)
m_dicItems.Add key, item
End Sub

End Class

‘’=========================================================================
‘’‘’ 这个,是存储文件域信息的的类。每一个name的文件,对应一个这样的类。
‘’=========================================================================
Class FileElement

‘’ m_开头,表示类成员变量。
Private m_strName
Private m_bData
Private m_strContentType
Private m_strFilePath
Private m_strFileName
Private m_lSize

‘’ Data是一个默认属性。目的是得到值
Public Default Property Get Data()
Data = m_bData
End Property

‘’ Name是得到文件域名称,就是<input type=file name=xxx>里的xxx
Public Property Get Name()
Name = m_strName
End Property

‘’ ContentType是得到文件contentType
Public Property Get ContentType()
ContentType = m_strContentType
End Property

‘’ FilePath是得到文件在客户端的路径
Public Property Get FilePath()
FilePath = m_strFilePath
End Property

‘’ FilePath是得到文件在客户端的路径
Public Property Get FileName()
FileName = m_strFileName
End Property

‘’ Size是得到文件大小
Public Property Get Size()
Size = m_lSize
End Property

Public Sub Add(name, data, contenttype, path)
m_strName = name
m_bData = data
m_strContentType = contenttype
m_strFilePath = path
m_strFileName = right(path, len(path)-instrrev(path, "\"))
m_lSize = lenb(data)
End Sub

End Class

‘’=========================================================================
‘’‘’ 这个,是我们模拟的request类。我们用它完成asp的request完成不了的任务 :)
‘’=========================================================================
Class UploadRequest

Private m_dicForms
Private m_dicFiles
Private m_bFormdata

Private Sub Class_Initialize()
Set m_dicForms = Server.CreateObject("Scripting.Dictionary")
Set m_dicFiles = Server.CreateObject("Scripting.Dictionary")
Call fill()
End Sub

‘’ 有了这个,就可以检查原始数据了
Public Property Get RawData()
RawData = m_bFormdata
End Property

‘’ 这一段丑陋的代码是为了实现outRequest.Forms.Count这个功能。
Public Property Get Forms()
Set Forms = New Counter
Forms.setCount(m_dicForms.Count)
End Property

‘’ 这一段丑陋的代码是为了实现outRequest.Files.Count这个功能。
Public Property Get Files()
Set Files = New Counter
Files.setCount(m_dicFiles.Count)
End Property

Public Property Get Form(index)
If isNumeric(index) Then ‘’是数字?用数字来检索
If index > m_dicForms.Count-1 Then
err.raise 1,"IndexOutOfBound", "表单元素索引越界"
End If
Items = m_dicForms.Items
Set Form = Items(index)
ElseIf VarType(index) = 8 Then ‘’字符串?也行!
If m_dicForms.Exists(index) Then ‘’存在,就返回值
Set Form = m_dicForms.Item(index)
Else ‘’不存在,就给个空值——request对象就是这么做的。
Exit Property
End If
Else ‘’给了一个不是数字也不是字符串的东东?出错!
err.raise 2,"IllegalArgument", "非法的表单元素索引"
End If
End Property

Public Property Get File(index)
If isNumeric(index) Then ‘’是数字?用数字来检索
If index > m_dicFiles.Count-1 Then
err.raise 1,"IndexOutOfBound", "文件元素索引越界"
End If
Items = m_dicFiles.Items
Set File = Items(index)
ElseIf VarType(index) = 8 Then ‘’字符串?也行!
If m_dicFiles.Exists(index) Then ‘’存在,就返回值
Set File = m_dicFiles.Item(index)
Else ‘’不存在,出错!
err.raise 2,"NullRef", "文件元素索引不存在"
End If
Else ‘’给了一个不是数字也不是字符串的东东?出错!
err.raise 2,"IllegalArgument", "非法的表单元素索引"
End If
End Property

Private Sub fill
‘’ 得到数据
m_bFormdata=request.binaryread(request.totalbytes)
‘’ 调用这个函数实现递归循环,读取文本/文件单元
Call fillEveryFirstPart(m_bFormdata)
End Sub

Private Sub fillEveryFirstPart(data)
‘’ 这就是name="
const_nameis=chrb(110)&chrb(97)&chrb(109)&chrb(101)&chrb(61)&chrb(34)
‘’ 这就是filename="
const_filenameis=chrb(102)&chrb(105)&chrb(108)&chrb(101)&_
chrb(110)&chrb(97)&chrb(109)&chrb(101)&chrb(61)&chrb(34)
‘’ 这是回车<return>
bncrlf=chrb(13) & chrb(10)
‘’ 得到divider,分隔符
divider=leftb(data,instrb(data,bncrlf)-1)
‘’ 起始位置
startpos = instrb(data,divider)+lenb(divider)+lenb(bncrlf)
‘’ 终止位置,从起始位置开始到下一个divider
endpos = instrb(startpos, data, divider)-lenb(bncrlf)
If endpos < 1 Then ‘’没有下一个了!结束!
Exit Sub
End If
part1 = midb(data, startpos, endpos-startpos)
‘’ 得到part1的第一行
firstline = midb(part1, 1, instrb(part1, bncrlf)-1)

‘’没有filename=",有name=",说明是一个文本单元(这里有一个BUG,自己研究一下?当作业吧)
If Not instrb(firstline, const_filenameis) > 0_
And instrb(firstline, const_nameis) > 0 Then
‘’ 得到表单域名称,就是<input type=sometype name=somename>里的somename。
fldname = B2S(midb(part1,_
instrb(part1, const_nameis)+lenb(const_nameis),_
instrb(part1, bncrlf)_
-instrb(part1, const_nameis)-lenb(const_nameis)-1))
‘’ 得到表单域的值
fldvalue = B2S(midb(part1,_
instrb(part1, bncrlf&bncrlf)+lenb(bncrlf&bncrlf),_
lenb(part1)-instrb(part1, bncrlf&bncrlf)+lenb(bncrlf&bncrlf)))
If m_dicForms.Exists(fldname) Then
Set fElement = m_dicForms.Item(fldname)
m_dicForms.Remove fldname
Else
Set fElement = new FormElement
End If

fElement.Add fldname&"_"&fElement.Count, fldvalue
m_dicForms.Add fldname, fElement

‘’有filename=",有name=",说明是一个文件单元(这里还是有一个BUG,研究出来没?)
ElseIf instrb(firstline, const_filenameis) > 0_
And instrb(firstline, const_nameis) > 0 Then
‘’ 得到表单域名称,就是<input type=file name=somename>里的somename。
fldname = B2S(midb(part1,_
instrb(part1, const_nameis)+lenb(const_nameis),_
instrb(part1, const_filenameis)_
-instrb(part1, const_nameis)-lenb(const_nameis)-3))
‘’ 得到表单域的值
fldvalue = midb(part1,_
instrb(part1, bncrlf&bncrlf)+lenb(bncrlf&bncrlf),_
lenb(part1)-instrb(part1, bncrlf&bncrlf)+lenb(bncrlf&bncrlf))
‘’ 得到路径
filepath = B2S(midb(part1,_
instrb(part1, const_filenameis)+lenb(const_filenameis),_
instrb(part1, bncrlf)_
-instrb(part1, const_filenameis)-lenb(const_filenameis)-1))
‘’ 得到contenttype
contenttype = B2S(midb(part1,_
instrb(part1, bncrlf)+lenb(bncrlf)+14,_
instrb(part1,_
bncrlf&bncrlf)-instrb(part1, bncrlf)-lenb(bncrlf)-14))
If lenb(fldvalue) > 0 Then ‘’size>0说明有文件传来了。
If m_dicFiles.Exists(fldname) Then
Set fElement = m_dicFiles.Item(fldname)
m_dicFiles.Remove fldname
Else
Set fElement = new FileElement
End If

fElement.Add fldname, fldvalue, contenttype, filepath
m_dicFiles.Add fldname, fElement
End If
End If

‘’ 截取剩下的部分,递归调用这个函数,来得到下一个part1。
Call fillEveryFirstPart(rightb(data, lenb(data)-endpos-1))
End Sub

‘’ 这是一个公用函数,作用是二进制和字符串的转换
Private Function B2S(bstr)
If not IsNull(bstr) Then
for i = 0 to lenb(bstr) - 1
bchr = midb(bstr,i+1,1)
If ascb(bchr) > 127 Then ‘’遇到了双字节,就得两个字符一起处理
temp = temp & chr(ascw(midb(bstr, i+2, 1) & bchr))
i = i+1
Else
temp = temp & chr(ascb(bchr))
End If
next
End If
B2S = temp
End Function

End Class

‘’ 这是一个辅助类,为了实现outRequest.Forms.Count功能。
Class Counter
Private m_iCnt

‘’ count是咱们这个类的一个只读属性
Public Property Get Count()
Count = m_iCnt
End Property

Public Function setCount(cnt)
m_iCnt = cnt
End Function
End Class
%>

<%
‘’下面是测试码
set outRequest = new UploadRequest
%>

<%=outRequest.Form(0).Name%>:<%=outRequest.Form("file1_desc")%><br>
<%=outRequest.Form(1).Name%>:<%=outRequest.Form("file2_desc")%><br>
<%=outRequest.Form(2).Name%>:<%=outRequest.Form(2).Count%><br>
<%=outRequest.Form(3).Name%>:<%=outRequest.Form(3)%>

一共有<%=outRequest.Forms.Count%>个文本单元<hr>

<%=outRequest.File(0).Name%>:
<%=outRequest.File("file1").ContentType%>:
<%=outRequest.File("file1").Size%>byte:
<%=outRequest.File("file1").FileName%>:
<%=outRequest.File("file1").FilePath%><br>

<%=outRequest.File(1).Name%>:
<%=outRequest.File("file2").ContentType%>:
<%=outRequest.File("file2").Size%>byte:
<%=outRequest.File("file2").FileName%>:
<%=outRequest.File("file2").FilePath%><br>

一共有<%=outRequest.Files.Count%>个文件单元<hr>

<%
‘’如果要测试文件1内容,可以:
‘’response.clear
‘’response.contenttype = outRequest.File("file1").ContentType
‘’response.BinaryWrite(outRequest.File("file1").Data)
‘’如果要测试文件2内容,可以:
‘’response.clear
‘’response.contenttype = outRequest.File("file2").ContentType
‘’response.BinaryWrite(outRequest.File("file2").Data)
%>

测试表单testform.html还用第三天那个。注意,每一个文本表单域和文件表单域都要填上,原因还是测试码给得很特殊,读了各个项目的值,测试了各个属性。不过,现实情况下,因为事先知道表单域的名称;即使不知道,也可以用outRequest.Forms.Count/outRequest.Files.Count来循环读取,所以是没问题的,不容易出错。

试试看!怎么样?成功!注意测试码最下边的部分,用来测文件内容的。分了两段,可以分别打开注释,进行测试哦。
现在,中英文文本都没有问题;文件也是各种都行,路径没限制。用法也很简单,很清晰。现在,文本域、文件域的读取就都解决了!

--------------------------------------------------------
今天这一段是很有意思的。结合第三天的内容,就可以看到解决upload问题的全貌了。这两次我都是越写越兴奋,放不下。不过,这可还没有结束哦!要做一个功能强大的东东出来,还需要限制上传文件尺寸、限制类型、存盘、入库等附加功能。所以,打起精神,让我们一鼓作气,把他彻底搞定!哦?!现在都一点啦,明天吧,呵呵。。。

==============================================================
第六天:附加功能

现在,核心功能已经实现了。但是,仅仅这样,还不能大幅度的提高我们的工作效率。一些常用的、重复的操作,象限制上传文件大小、类型以及文件存盘、入库等还是应该统一处理。所以,当前的目标,就是封装常用的功能,尽量让他好用。

首先,我们看看上传限制的实现。我们要控制的,有文件大小,和文件类型。大小很容易控制,只要在每次读取文件放进FileElement类的时候(执行Add方法的时候),看一下它的Size,并且适时的抛出异常就可以了;文件类型控制类似,不过需要判断一下扩展名(当然也可以利用contentType,不过更依赖机器配置——每一种机器可以识别的contentType各有不同)。

然后,就是结果的永久性保存了。为了灵活起见,入库就不再封装,由用户解决;存盘因为它是原子操作(本身不可分割,而且也不依赖其他操作),可以比较好的封装。我们可以在UploadRequest类提供SaveTo(serverpath)方法,用来一次性保存所有图片;另外在FileElement类里提供SaveTo(serverpath)和SaveAs(serverpath, newfilename)方法,分别实现按照原文件名保存图片以及按照指定文件名保存文件的功能。

考虑到上传控制的问题,把读取数据的fill方法放到Class_Initialize已经不合适了。我们另做一个Upload方法,进行文件上传的具体操作。这样,就可以在上传之前,对ourRequest进行设置。新的类设计如下:
A。UploadRequest(上面设计过,这里是扩充)
这个类和request对象是对应的
属性:
RawData 得到原始数据,方便检查[只读]
Forms 得到一个有count属性的计数器,
可以用outRequest.Forms.Count的方式,得到文本表单域的的个数[只读]
Files 得到一个有count属性的计数器,
可以用outRequest.Files.Count的方式,得到文件表单域的的个数[只读]
Form(index) 可以用数字或文本检索文本表单域,做用类似request.form。
他返回一个FormElement型的对象[只读]
File(index) 可以用数字或文本检索文件表单域,他返回一个FileElement型的对象[只读]
TotalBytes 得到所有文件总大小[只读]
AllowedFilesList 设置允许上传的扩展名[只写]
DeniedFilesList 设置不允许上传的扩展名(和AllowedFilesList任取一个就行了)[只写]
MaxFileSize 设置允许上传的每个文件的大小[只写]
TotalMaxFileSize 设置允许上传的所有文件的大小[只写]
方法:
Upload 上传分拆的具体实现方法
SaveTo(path) 保存所有的文件到指定路径(按原名)

B。FileElement
可以把它看成单个文件域的化身。通过这个类,可以得到详细的文件信息,比如name,data,path,filename,contentType,size等等。
属性:
Name 文件域的名称。就是<input type=file name=xxx>里的xxx[只读]
Data 文件域的内容。二进制串[只读]
ContentType 文件域的contentType[只读]
FilePath 文件域包含的文件在客户机上的全路径[只读]
FileName 文件域包含的文件的文件名[只读]
Size 文件域包含的文件的尺寸[只读]
方法:
SaveTo(path) 保存当前文件到指定路径(按原名)
SaveAs(path, name) 按给定文件名保存当前文件到指定路径,如果存在,就覆盖
SaveWithoutOverwrite(path, name) 按给定文件名保存当前文件到指定路径,不覆盖

保存文件的时候,因为它是二进制流,所以,只能用于文本操作的fso是不能用的,这里,我们用到了ado的stream对象,他也是唯一的选择。但是,一定要注意,因为新版ado才有他,所以,老的系统可能不能正确的进行文件的保存。如果提示了stream对象的问题,请升级MDAC,或者干脆放弃这个功能。因为我们之所以用无组件的方法,就是不想在server上配置。装MDAC本身已经偏离了目标。

现在,只要实现了这几个新方法和属性,我们的无组件上传就可以说是大功告成了。明天,我们就最终实现这个功能完善的类,并且把前两天没有注意到的细节进行一些修补。其实明天的内容不多,新的知识只有stream对象的用法。这些,在论坛里以前就有提及,如果不是很清楚,可以翻看一下论坛的旧贴,或是到http://www.2yup.com/asp/referrence/index.asp下一个ADO参考看看,相信会揭开你的心中疑团。

OK,让我们一起期待明天吧! ^&^

原文转自:http://www.ltesting.net