在实现下载类程序的过程中,有时需要通过php读取文件,随后推送给客户端实现文件的下载。本文介绍PHP实现该项功能的两种常见方法。
场景分析
相比直接暴露给用户文件真实路径,基于PHP文件数据下载可能基于以下原因:
- 不想暴露文件真实路径,避免多线程下载。
- 下载前需要验证用户权限。
- 进行下载量统计。
- 服务器端打乱文件名存储,下载时对下载文件进行重命名。
但是同时,劣势也是明显的:
- 不能支持多线程下载:意味着使用各类下载工具如迅雷、IDM等会受到影响,但浏览器单线程下载正常。
- 会占用较高的服务器资源:硬性限制,无法减少。
- 受到PHP的memory_limit配置限制:可以在页面内使用
ini_set('memory_limit', '512M');
婉转解决。
所以,在使用此类方法进行下载时,应周详考虑场景,是否能接受以上限制。
代码实现
以下代码在PHP8.1.2中测试。假设文件路径变量为 $filePath,存储所在位置字符串如 ‘/uploads/test.zip’
核心的思路是先构造文件基本信息,输出包括文件名、文件类型、大小等信息,通过header传输。随后通过readfile
或stream_copy_to_stream
函数进行输出。
关于重命名文件,可以在Content-Disposition进行构造,如下方代码所示。
构造头部
header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename=' . urlencode('新文件名') . '.' . pathinfo($filePath, PATHINFO_EXTENSION)); header('Content-Transfer-Encoding: Binary'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header('Pragma: public'); header('Content-Length: ' . filesize($filePath));
通过读取文件方式处理:
ob_clean(); flush(); readfile($filePath);
或通过数据流方式处理:
ob_clean(); flush(); $readableStream = fopen($filePath, 'r'); $writableStream = fopen('php://output', 'w'); stream_copy_to_stream($readableStream, $writableStream);
stream_copy_to_stream在广泛认知中,适用于大型文件的复制,相比读取文件方式其占用内存会更少,甚至可以突破memory_limit的限制。
但请注意,经过我多次测试,在这种下载场景中,首先memory_get_peak_usage(),readfile稍优于stream几百个字节(我没写反),几乎可以忽略不计,且steam不能突破memory_limit的限制。
综合考虑,建议使用readfile。
另外,应该考虑,如果下载文件后没有其它操作,应该 exit();
一个较为完整的代码片段
if (file_exists($filePath)) { header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename=' . urlencode(str_replace(' ', '_', $file['title'])) . '.' . pathinfo($filePath, PATHINFO_EXTENSION)); header('Content-Transfer-Encoding: Binary'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header('Pragma: public'); header('Content-Length: ' . filesize($filePath)); ob_clean(); flush(); readfile($filePath); exit(); } else { exit('File does not exist. Please report to the web administrator. Thanks.'); }
其它
考虑到网速缓慢的用户,和突破PHP内存限制的问题,可选在文件顶部添加:
set_time_limit(0);
ini_set(‘memory_limit’, ‘512M’);
实践中,我会建议客户,为超过30M的文件添加备用网盘下载地址,建议优先使用网盘下载。
另外,下载程序中,如果本地文件超过30M,自动变为暴露文件真实下载路径的形式。
参考资料
https://stackoverflow.com/questions/7263923/how-to-force-file-download-with-php