文件上传是WEB开发中经常要用到的功能但ASP本身和内置的组件都不支持文件上传功能网上流传的一些第三方组件虽然能够解决这个问题但大多是要收费的更别说Open Source了本文将详细剖析WEB文件上传的原理以及一步步指导读者如何用Delphi开发一个ASP上传组件
源码和demo我已经发布在个人主页上http://wwwwushuangnet.
一Html文件分析
首先我们来看一个html文件源码文件名是testhtm功能是提供用户上传的界面
<html>
<body>
<center>
<form name=mainForm enctype=multipart/formdata
action=testasp method=post>
<input type=file name=mefile><br>
<input type=hidden name=a value=fdsaf>
<input type=hidden name=a value=fdsaf>
<input type=hidden name=a value=fdsaf>
<input type=hidden name=a value=fsdfsdsaf>
<input type=hidden name=a value=这个是这个>
<input type=text name=a value=fdsaf>
<input type=submit name=ok value=OK>
</form>
</center>
</body>
</html>
这个文件里包含了一个名为mainForm的form以及随手写的一些input域注意这个form和一般的form有两个不同的地方一是它有一个type=file的域没有value用浏览器打开这个文件时这个域会表现为一个右侧有浏览字样的文件输入框用户可以通过它来选择本地硬盘上的文件二是form有一个特殊的属性enctype=multipart/formdata这个属性告诉浏览器要上传二进制文件并进行相应编码
这种编码会产生什么样的表单信息呢?让我们来看看testasp也就是接受表单的asp文件的源码它非常简单
<%
formsize=requesttotalbytes 获得表单原始信息的长度
formdata=requestbinaryread(formsize) 读取表单原始信息
responsebinarywrite formdata返回表单原始信息
%>
如读者在注释中了解的这段代码的功能是将表单的原始信息返回让我们来看看它的运行效果将这两个文件置于web目录下访问testhtm在文件输入框中选择一个文件(我选了一个jpg图片不过最大不要太大)提交然后可以看到这样一堆乱七八糟的信息
de ContentDisposition: formdata;
name=mefile; filename=C:\Documents and Settings\aaa\My Documents\My
Pictures\zzjhjpg ContentType: image/pjpeg (作者注以下为乱码)
de ContentDisposition: formdata;
name=a fdsaf de ContentDisposition:
formdata; name=a fdsaf de
ContentDisposition: formdata; name=a fdsaf
de ContentDisposition: formdata;
name=a fsdfsdsaf de
ContentDisposition: formdata; name=a 这个是这个
de ContentDisposition: formdata;
name=a fdsaf de ContentDisposition:
formdata; name=ok OK de
这就是用multipart/formdata方式编码的表单原始信息其中那一段看起来是乱码的部分就是jpg图片的编码
分析一下这段信息的格式
de 这是各个域之间的分隔符
ContentDisposition: formdata; 说明这是表单中的域
name=mefile; 域的名称
filename=C:\Documents and Settings\aaa\My Documents\My Pictures\zzjhjpg 上
传文件在本地硬盘上的名称
ContentType: image/pjpeg 文件类型
后面是文件本身的数据
其它各个域的信息也可以以此类推
众所周知在ASP中使用request对象可以访问用户提交表单的各个域因为request对象会对原始的表单信息进行解析提取出表单中每个域的值但是request并不能解析这multipart/formdata格式的表单信息这就是ASP不能直接支持文件上传的原因所在读者可以试试在testasp中用request(mefile)这样的格式是不能读取到正确的信息的
问题的症结已经找到解决的思路也很简单用Delphi开发一个COM组件接受这种原始表单信息将各个域一一提取出来返回给asp文件也就是完成request对象没有完成的功能
二用Delphi开发组件
Delphi对开发ASP组件提供了极好的支持大大简化了我们的开发过程
启动Delphi 选择FileNewOtherActiveXActiveX Library这样就建立了一个ActiveX库将此Library改名为myobj存盘选择FileNewOtherActiveXActive Server Object在CoClassname中填入upfile确定这时会跳出一个标题为myobjtlb的对话框这是Delphi特有的以可视化方式编辑COM接口的功能用Delphi开发过COM的读者应该比较熟悉
在myobj下的名为Iupfile的Interface下添加个属性和一个方法如果不懂得如何操作请参见Delphi参考书的相关部分按F可以看到生成的相应的myobj_tlbpas文件其中的Iupfile接口应该是这个样子
Iupfile = interface(IDispatch)
[{CDEBAAEAEB}]
procedure OnStartPage(const AScriptingContext: IUnknown); safecall;
procedure OnEndPage; safecall;
function Get_Form(Formname: OleVariant): OleVariant; safecall;
function Get_FileName: OleVariant; safecall;
function Get_FileSize: Integer; safecall;
procedure FileSaveAs(FileName: OleVariant); safecall;
function Get_FileData: OleVariant; safecall;
function Get_FileType: OleVariant; safecall;
property Form[Formname: OleVariant]: OleVariant read Get_Form;
property FileName: OleVariant read Get_FileName;
property FileSize: Integer read Get_FileSize;
property FileData: OleVariant read Get_FileData;
property FileType: OleVariant read Get_FileType;
end;
其中的OnStartPage方法和OnEndPage方法是Delphi默认生成的其它的是手动加入的
切换到unitpas(也是Delphi自动生成的)改名为upfilepas存盘可以看到存在一个Tupfile类的声明它是继承自TASPObject类和Iupfile接口的Delphi 已经自动生成了相应的代码接下来的任务就是实现这个接口
除了完成Iupfile接口中的属性和方法之后还需要补充一些东西以便完成我们的任务最终的Tupfile类的声明如下
Tupfile = class(TASPObject Iupfile)
public
protected
procedure OnEndPage; safecall; //页面开始
procedure OnStartPage(const AScriptingContext: IUnknown); safecall; //页面
结束
procedure FileSaveAs(Filename: OleVariant); safecall; //保存文件
function Get_Form(Formname: OleVariant): OleVariant; safecall; //
function Get_FileName: OleVariant; safecall;
function Get_FileSize: Integer; safecall;
function Get_FileData: OleVariant; safecall;
function Get_FileType: OleVariant; safecall;
private
FContentData:string;
FFileDataFFileNameFFileType:string;
FFormInfo:TStringList;
function instr(strstr:string;startpos:integer):integer;
procedure AnalyFormData(content:string);
end;
下面我们来一一分析这些成员的具体实现
procedure TupfileOnStartPage(const AScriptingContext: IUnknown);
var
AOleVariant : OleVariant;
tmpvar : OleVariant;
contentlength : integer;
iDeliCountposposlastpos : integer;
FDelimeter : string;
begin
inherited OnStartPage(AScriptingContext);
FFormInfo := TStringListCreate;
contentlength := RequestTotalBytes;
AOleVariant := contentlength;
tmpvar := RequestBinaryRead(AOleVariant);
for i := to contentlength do
begin
FContentData := FContentData + chr(byte(tmpvar[i]));
end;
pos := pos(##FContentData);
FDelimeter := copy(FContentDatapos+);
DeliCount := length(FDelimeter);
lastpos := ;
pos:=;
while pos>=pos do
begin
pos := instr(FDelimeterFContentDatalastpos);
if pos = then Break;
pos := pos + DeliCount;
pos := instr(FDelimeterFContentDatapos);
AnalyFormData(copy(FContentDatapospospos));
lastpos := pos;
end;
end;
前面说过OnStartPage方法是Delphi自动生成的在装载页面时发生在这个方法中我们完成一些初始化的任务读取表单的原始数据解析表单中的域并存入相应的属性中以备调用
由于Delphi已经对ASP中的对象进行了很好的封装所以即使在Delphi环境下也可以方便地调用它们就象在ASP中一样例如RequestTotalBytes首先将原始表单数据读入到一个OleViarians类型的tmpvar中然后通过一个循环将它转换为Delphi中的string格式并存放在FContentData中
接下来通过查找换行符解析出分隔符的内容和长度然后在一个循环中用AnalyFormData成员函数一一解析出每个域初始化工作就这样完成了
再看AnalyFormData函数的实现
procedure TupfileAnalyFormData(content: string);
var
pospos:integer;
FormNameFormValue:string;
isFile:boolean;
begin
isFile := false;
pos := instr(name=content)+;
pos := instr(contentpos);
FormName := copy(contentpospospos);
//检查是否文件
pos := instr(filename=contentpos+);
if pos <> then
begin
isFile := true;
pos := pos + ;
pos := instr(contentpos);
FFilename := copy(contentpospospos);
end;
pos := instr(####contentpos+)+;
FormValue := copy(contentposlength(content)pos);
if isfile then
begin
FFileData := FormValue;
//查找文件类型信息
pos := instr(ContentType: contentpos+);
if pos <> then
begin
pos := pos + ;
FFileType := copy(contentpospospos);
end;
end
else
begin
FFormInfoadd(FormName+=+FormValue);
end;
end;
如注释中所表达的AnalyFormData提取原始数据中的域如果是域是文件类型则将文件类型和文件数据分别放入FFileType和FFileData中如果是其它类型则将名称和值放入一个TStringlist类型的FFormInfo中FFormInfo中维护着除文件类型外的所有域的信息以名称=值的格式存放
function TupfileGet_Form(Formname: OleVariant): OleVariant;
begin
Result := FFormInfoValues[Formname];
end;
这个函数返回域的值只需要简单地调用FFormInfo的values方法就可以得到相应的值这是在Tstringlist类内部实现的
function TupfileGet_FileName: OleVariant;
begin
Result := ExtractFileName(FFileName);
end;
function TupfileGet_FileSize: Integer;
begin
Result := length(FFileData);
end;
function TupfileGet_FileData: OleVariant;
var
i:integer;
begin
Result := VarArrayCreate( [length(FFileData)] varByte );
for i := to length(FFileData) do
begin
Result[i] := Byte(FFileData[i+]);
end;
end;
这三个函数分别返回文件的名称大小数据要注意的是在返回文件数据时必须进行相应的转换将Delphi中的string类型转换为OleVariant类型
procedure TupfileFileSaveAs(Filename: OleVariant);
var
fsout:TFileStream;
i:integer;
afile:file of byte;
begin
fsout := TFileStreamCreate(Filenamefmcreate);
for i := to length(FFileData) do
begin
fsoutWrite(Byte(FFileData[i]))
end;
fsoutFree;
end;
这个方法将文件保存到服务器上的磁盘
编译myobj这个project得到一个myobjdll文件开发工作就此完成
三使用ASP上传组件
在命令行下输入regsvr myobjdll弹出一个对话框告诉你组件已经注册如果找不到regsvrexe这个文件它在windows\system或winnt\system目录下
将本文开头提到的testasp文件修改为如下内容
<%建立对象
Set upfile = ServerCreateObject(myobjupfile)
获得表单对象
responsewrite upfileform(a)&<br>
responsewrite upfileform(a)&<br>
responsewrite upfileform(a)&<br>
responsewrite upfileform(a)&<br>
responsewrite upfileform(a)&<br>
responsewrite upfileform(a)&<br>
获得文件大小
responsewrite 文件字节数&upfilefilesize&<br>
获得文件类型
responsewrite 文件类型&upfilefiletype&<br>
获得文件名保存文件
upfilefilesaveas(ServerMapPath()+upfilefilename)
set upfile = nothing
%>
再次访问testhtm提交表单现在你可以看到相关的返回信息并且在服务器上testasp所处的目录下找到上传的文件
这个组件只能上传单个文件但根据同样的原理一次上传多个文件的功能也是不难实现的有兴趣的读者可以自行尝试