七阶子博客: 杂文 | 游戏 | 戏剧 | 白蛇 | 文艺 | 编程 | 近期
请输入标题关键字或 yyyymmdd 格式的日期

    Perl CGI 保存上传文件功能探讨

    读 CPAN 上 CGI 的文档,perl 的 CGI 模块是个强大、成熟却不够现代、似乎显得有点过时的技术了。尤其不建议再用 CGI 模块的 html 文本生成函数等功能,建议改用模板技术生成 html 页面。且高版本的 perl 已经不内置 CGI 了,需求像普通大型模块一样另行安装。

    其实,以单线程、克隆进程方式的 CGI 脚本编写很简单,自行读取各环境变量与处理标准输入输出即可。没必要安装庞大的 CGI 模块系统,例如要读取 GET 参数,只要如下两行代码即可(也可写成一行)。

    my $query = $ENV{QUERY_STRING};
    my %query = map {$1 => $2 if /(\w+)=(\S+)/} split(/&/, $query);
    

    其语义是将类似 key1=val1&key2=val2&key3=val3 的参数解析为一个哈栖(hash)数据结构,不过在实际使用中可能还要进行 URI 转义,如将 %20 恢复为一个空格。

    普通表单通过 POST 提交时,所有参数也拼成一行,与 GET 参数相同的格式,只要从标准输入读取后可类似解析。只是当 CGI 脚本要处理复杂表单时,要上传文件时,表单内容需要用 multipart/form-data 编码,这就比较复杂了,大致如下:

    -----------------------------1273969829406
    Content-Disposition: form-data; name="userfile"; filename="上传文件名.sql"
    Content-Type: application/octet-stream
    被上传文件内容......
    被上传文件内容......
    -----------------------------1273969829406
    Content-Disposition: form-data; name="key"
    普通表单元素值
    -----------------------------1273969829406
    Content-Disposition: form-data; name="submit"
    提交按钮也有值
    -----------------------------1273969829406--
    

    第一行是分隔符,末尾那串数字是自动生成的,以保证分界符不会与内容冲突。上传的文件内容及其他各个表单元素都用这行分界符分开,并且每部分还有一行说明。

    理论上,也可以手写 perl 代码解析之。但有点复杂,就不必矫情不用 CGI 模块了。样例代码如下:

    use CGI;
    my $cgi = CGI->new();
    if (my $io_handle = $cgi->upload($file_field)) {
        # while(<$io_handle>) { todo; }
    	my $buffer;
    	open ( my $out_file, '>', "$Bin/upload_io.save" );
    	while ( my $bytesread = $io_handle->read($buffer,1024) ) {
    		print $out_file $buffer;
    	}
    }
    my $filename = $cgi->param($file_field);
    my $tmpfilename = $cgi->tmpFileName( $io_handle )
    

    当创建 CGI 对象时,模块自动处理了从环境变量处接收到的参数,及从标准输入接收到的 POST 内容。也处理好了上传文件,它将上传文件先保存在临时文件中,并打开该临时文件,可供脚本使用该文件句柄。

    方法 upload 获取的是文件句柄,参数是表单的域名,通过文件句柄还可获得临时文件名(服务器上的文件名),方法 param 获取的是浏览器上传的客户端文件名,类似于其他表单元素的简单域值。

    获知了临时文件名,可以想到通过重命名来保存文件。如果脚本不主动重命名,脚本结束后临时文件是应该删除的。不过在 perl 脚本用 rename 重命名在跨文件系统(如不同硬盘分区)可能会有问题,而且也未能修改文件权限。所以重命名临时文件没有初始想得那么简单。

    所以更常规的用法就是操作文件句柄,不管是想读取文件内容根据上传内容进行业务处理,还是另外写入一个新文件句柄,都是当作常规文件操作。如果确认是文本文件,可以直接用 Perl 的著名“钻石”操作符 <$handle> 读取每一行。但如果是二进制文件,<>操作符就不太合适了,建议就按文档示例每次读 1024 字节吧。