实现PHP读取文件并下载的方法(重命名下载文件)

在实现下载类程序的过程中,有时需要通过php读取文件,随后推送给客户端实现文件的下载。本文介绍PHP实现该项功能的两种常见方法。

场景分析

相比直接暴露给用户文件真实路径,基于PHP文件数据下载可能基于以下原因:

  1. 不想暴露文件真实路径,避免多线程下载。
  2. 下载前需要验证用户权限。
  3. 进行下载量统计。
  4. 服务器端打乱文件名存储,下载时对下载文件进行重命名。

但是同时,劣势也是明显的:

  1. 不能支持多线程下载:意味着使用各类下载工具如迅雷、IDM等会受到影响,但浏览器单线程下载正常。
  2. 会占用较高的服务器资源:硬性限制,无法减少。
  3. 受到PHP的memory_limit配置限制:可以在页面内使用 ini_set('memory_limit', '512M'); 婉转解决。

所以,在使用此类方法进行下载时,应周详考虑场景,是否能接受以上限制。

代码实现

以下代码在PHP8.1.2中测试。假设文件路径变量为 $filePath,存储所在位置字符串如 ‘/uploads/test.zip’

核心的思路是先构造文件基本信息,输出包括文件名、文件类型、大小等信息,通过header传输。随后通过readfilestream_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

点赞