博客列表 >PHP(多)文件上传实现和函数封装

PHP(多)文件上传实现和函数封装

吾逍遥
吾逍遥原创
2020年12月12日 11:34:221002浏览

一、PHP 文件上传的相关知识

对 PHP 文件上传的相关知识总结主要是参考老师演示的代码和 drawer.php(某大神写的 PHP 单文件版的服务器文件管理端)

1. php 关于文件上传的配置

文件上传项目项在php.ini中设置,常用的配置项有:

序号 配置项 默认值 描述
1 file_uploads On 使 PHP 支持文件上传
2 upload_tmp_dir /tmp 指示应该临时把上传的文件存储在什么位置
3 max_file_uploads 20 单次请求时允许上传的最大文件数量
4 max_execution_time 30 设置脚本被解析器终止之前 PHP 最长执行时间(秒) ,防止服务器资源被耗尽
5 max_input_time 60 设置 PHP 通过 POST/GET/PUT 解析接收数据的时长(秒)
6 memory_limit 128M 系统分配给当前脚本执行可用的最大内存容量
7 post_max_size 8M 允许的 POST 数据的总大小
8 upload_max_filesize 32M 允许的尽可能最大的文件上传

2. 服务端超全局变量$_FILES

  • 上传文件的描述信息,全部保存在系统全局变量$_FILES
  • $_FILES以二维数组形式保存: $_FILES['form_file_name']['key']
  • 'form_file_name': 对应着表单中<input type="file" name="my_pic">name属性值
  • 'key': 共有 5 个键名, 描述如下:
序号 键名 描述
1 name 文件在客户端的原始文件名(即存在用户电脑上的文件名)
2 type 文件的 MIME 类型, 由浏览器提供, PHP 并不检查它
3 tmp_name 文件被上传到服务器上之后,在临时目录中临时文件名
4 error 和该文件上传相关的错误代码
5 size 已上传文件的大小(单位为字节)
  • 文件上传错误信息描述
序号 常量 描述
1 UPLOAD_ERR_OK 0 没有错误发生,文件上传成功
2 UPLOAD_ERR_INI_SIZE 1 文件超过php.iniupload_max_filesize
3 UPLOAD_ERR_FORM_SIZE 2 文件大小超过表单中MAX_FILE_SIZE指定的值
4 UPLOAD_ERR_PARTIAL 3 文件只有部分被上传
5 UPLOAD_ERR_NO_FILE 4 没有文件被上传
6 UPLOAD_ERR_NO_TMP_DIR 6 找不到临时文件夹
7 UPLOAD_ERR_CANT_WRITE 7 文件写入失败

3、介绍一些常用的 PHP 函数

与 php.ini 配置相关的函数: 我们知道修改 php.ini 后要重启服务,但修改配置一般是某些页面需求,再者一般也不建议随意修改 php.ini 的配置文件。PHP 提供了 ini 为前缀的函数,修改配置仅对当前页面有效,页面释放后就无效了,非常适合我们平常使用。

  • ini_set(string $varname,string $newvalue):string 设置指定配置选项的值。这个选项会在脚本运行时保持新的值,并在脚本结束时恢复。简单的说就是可以临时修改 pnp.ini 配置文件中的值,页面结束时恢复 。这样就可以不用去修改 php.ini 的默认配置了,毕竟它是全局的,影响机器上所有 PHP 服务,而我们改变一般都是针对当前需求的,所以使用它修改比较合适。
  • ini_get(string $varname):string获取一个配置选项的值,ini_get_all([ string $extension[, bool $details = true]]):array获取所有已注册的配置选项,get_cfg_var(string $option):mixed获取 PHP 配置选项 option 的值,此函数不会返回 PHP 编译的配置信息,或从 Apache 配置文件读取。
  • ini_restore(string $varname):void 恢复指定的配置选项到它的原始值。
  1. //限制可访问目录,避免恶意修改
  2. ini_set('open_basedir',__DIR__);//仅在当前页面中应用该配置,不影响PHP.ini配置文件中设置,页面结束后就无效了。
  3. echo ini_get('open_basedir'),'<br>';
  4. ini_set('max_file_uploads','30');//设置无效
  5. echo ini_get('max_file_uploads'),'<br>';

补充: 本以为可以设置文件上传相关配置,经测试发现无效,查 PHP 官方只有可修改范围是 PHP_INI_ALL 才可以被 ini_set 修改。就当了解知道吧。

与目录或文件相关的函数: PHP 内置大量的文件系统操作函数,在一篇类的自动加载时已经介绍过了 is_file 和 file_exists 函数,下面再介绍与上传文件相关的函数

  • 目录相关的函数:is_dir()判断给定文件名是否是一个目录,如果文件名存在,并且是个目录返回 true,否则返回 false。opendir()、 closedir()、readdir()和rewinddir()对目录进行遍历。rmdir()删除目录,mkdir新建目录。
  1. /**
  2. * 遍历出所有文件或文件夹
  3. * @access public
  4. * @param string $dira 要遍历的文件夹名
  5. * @return array
  6. */
  7. function traverseDir( $dira ) {
  8. $arr = array();
  9. if ( $dh = opendir( $dira ) ) {
  10. while ( ( $file = readdir( $dh ) ) !== false ) {
  11. if ( ( $file != '.' ) && ( $file != '..' ) && is_dir( $dira . '/' . $file ) ) {
  12. $arr[] = $dira . '/' . $file;
  13. foreach ( traverseDir( $dira . '/' . $file ) as $v ) {
  14. $arr[] = $v;
  15. }
  16. }
  17. clearstatcache();
  18. }
  19. }
  20. return $arr;
  21. }
  22. printf("<pre>%s<pre>",print_r(traverseDir('F:/120910'),true));
  • 文件相关的函数: 判断就是 is_file 和 file_exists 两个函数了。copy()复制文件,如果目标文件已存在,将会被覆盖。这里重点说下对上传临时文件的操作函数即tmp_name键名的临时文件
    • getimagesize(临时文件) 检验是否真实图片,可防止修改扩展名伪装图片
    • is_uploaded_file(临时文件) 判断文件是否是通过 HTTP POST 上传的,如果 filename 所给出的文件是通过 HTTP POST 上传的则返回 true。这可以用来确保恶意的用户无法欺骗脚本去访问本不能访问的文件,例如/etc/passwd
    • move_uploaded_file(临时文件,目标文件 将上传的文件移动到新位置。
  1. if (!getimagesize($fileInfo['tmp_name'])) die('不是真实图片,get out~');
  2. if (!is_uploaded_file($fileInfo['tmp_name'])) die('上传方式错误:请使用http post方式上传');
  3. if (!move_uploaded_file($fileInfo['tmp_name'], $fileRealPath)) die('文件上传失败');
  • 路径相关的函数:
    • basename(string $path[,string $suffix]):string给出一个包含有指向一个文件的全路径的字符串,本函数返回基本的文件名。
    • dirname(string $path[,int $levels = 1]):string给出一个包含有指向一个文件的全路径的字符串,本函数返回去掉文件名后的目录名,且目录深度为 levels 级。
    • pathinfo(string $path[,int $options = PATHINFO_DIRNAME | PATHINFO_BASENAME | PATHINFO_EXTENSION | PATHINFO_FILENAME ]):mixed返回一个关联数组包含有 path 的信息。返回关联数组还是字符串取决于 options。本函数检查并确保由 filename 指定的文件是合法的上传文件(即通过 PHP 的 HTTP POST 上传机制所上传的)。如果文件合法,则将其移动为由 destination 指定的文件。
  1. // 路径
  2. echo basename("/etc/sudoers.d").PHP_EOL;
  3. echo basename("/etc/sudoers.d", ".d").PHP_EOL;//如果文件名是以第二个参数结束的,那这一部分也会被去掉
  4. echo dirname("/etc/passwd") . PHP_EOL;
  5. echo dirname("/usr/local/lib", 2). PHP_EOL;//第二个参数指示深度(PHP7.0开始支持)
  6. $path_parts = pathinfo('/www/htdocs/inc/lib.inc.php');
  7. echo $path_parts['dirname'].PHP_EOL; //返回/www/htdocs/inc
  8. echo $path_parts['basename'].PHP_EOL; //返回lib.inc.php
  9. echo $path_parts['extension'].PHP_EOL; //返回php
  10. echo $path_parts['filename'].PHP_EOL; //返回lib.inc

二、PHP(多)文件上传前端的实现

1、功能和源码

功能描述:

  • 可选择一个文件,也可选择多个文件
  • 若选择文件是图片则提供预览功能
  • 对选择文件可进行上传服务端
  1. <style>
  2. .container {
  3. width: 70vw;
  4. min-width: 600px;
  5. margin: 3em auto;
  6. background-color: #007d20;
  7. color: white;
  8. font-size: 1.1em;
  9. padding: 0.5em 1em 3em;
  10. border-radius: 1em;
  11. }
  12. .container h2 {
  13. text-align: center;
  14. margin: 0;
  15. padding: 0;
  16. border: none;
  17. }
  18. form fieldset {
  19. display: flex;
  20. justify-content: space-between;
  21. }
  22. form fieldset#image {
  23. flex-flow: row wrap;
  24. justify-content: initial;
  25. /* flex其实也有弹性单元的概念,不过这不是官方的说法,它的提出是我学习Grid时发现的,只是它是交叉方向的,不设置默认是拉伸stretch */
  26. /* 在项目中若不设置,则图片宽度和高度都一样,这样有的图片就被拉伸了,不是按图片比例缩放 */
  27. align-items: center;
  28. }
  29. form fieldset#image img {
  30. margin: 5px 10px;
  31. }
  32. </style>
  33. <div class="container">
  34. <h2>PHP实现的(多)文件上传</h2>
  35. <form action="upload.php" method="POST" enctype="multipart/form-data">
  36. <fieldset>
  37. <legend>选择上传文件</legend>
  38. <input type="file" id="file" name="js_file[]" multiple />
  39. <button>上传服务器</button>
  40. </fieldset>
  41. <fieldset id="image" style="display: none;"></fieldset>
  42. </form>
  43. </div>
  44. <script>
  45. const file = document.querySelector('#file');
  46. const fdset = document.querySelector('#image');
  47. file.addEventListener('change', showImage, false);
  48. function showImage() {
  49. // 上传文件信息保存在input的DOM的files属性中
  50. let files = file.files;
  51. let imgArr = [];
  52. for (let item of Object.values(files)) {
  53. if (item.type.includes('image')) imgArr.push(item);
  54. }
  55. fdset.setAttribute('style', 'display:none;');
  56. if (imgArr.length > 0) {
  57. // 先清空内容再刷新
  58. fdset.setAttribute('style', 'display:block;');
  59. fdset.innerHTML = null;
  60. const legend = document.createElement('legend');
  61. legend.innerHTML = `准备上传${imgArr.length}张图片`;
  62. fdset.appendChild(legend);
  63. for (let [index, item] of imgArr.entries()) {
  64. const reader = new FileReader();
  65. reader.readAsDataURL(item);
  66. reader.onload = () => {
  67. const img = new Image();
  68. // const img=document.createElement('img');
  69. // reader.result为获取结果
  70. img.src = reader.result;
  71. img.width = '150';
  72. // 将用户选择的图片显示到指定元素中
  73. fdset.appendChild(img);
  74. };
  75. }
  76. }
  77. }
  78. </script>

2、前端图片的预览

MDN 官方介绍:FileReader 对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。其中File对象可以是来自用户在一个<input>元素上选择文件后返回的FileList对象,也可以来自拖放操作生成的 DataTransfer对象,还可以是来自在一个HTMLCanvasElement上执行mozGetAsFile()方法后返回结果。 重要提示: FileReader仅用于以安全的方式从用户(远程)系统读取文件内容 它不能用于从文件系统中按路径名简单地读取文件。 要在JavaScript中按路径名读取文件,应使用标准Ajax解决方案进行服务器端文件读取,如果读取跨域,则使用CORS权限。

input 控件的上传的 FileList 保存了文件名,没有保存路径,读取只能通过 FileReader,然后通过readAsDataURL读取指定的 Blob 或 File 对象,成功时返回结果中 result 属性将包含一个data:URL格式的字符串(base64 编码)以表示所读取文件的内容。然后加载给 img 的 src,在 js 中new Image()也可以创建 HTMLImageElement,或直接document.createElement('img')创建,上面代码中二种形式均支持,而且 img 元素比较特殊,既支持路径名的图片,也支持图上的 base64 编码。

上面图片预览代码是在老师演示代码基础上进行了改进:

  • 图片 HTMLImageElement 创建,老师演示是new Image(),我增加了document.createElement('img'),二者经测试是等效的
  • 多张图片是 Flex 布局,由于其默认等高,只设置了宽度,图片并不会等比例缩放,需要在 CSS 中设置align-items:center;。关于 Flex 弹性单元的概念,首先它不是官方的,但确实存在的,是我通过 Grid 相对提出的,具体可见https://www.php.cn/blog/detail/24670.html
  • 上面演示的是实时添加 DOM,本想使用文档片断(DocumentFragment)来优化加载渲染,但由于图片是异步加载的,目前没解决,以后再探讨吧

三、PHP(多)文件上传后端的实现

1、功能和源码

功能描述:

  • 可处理单文件或多文件上传
  • 对文件上传失败可提供详细信息
  • 若是图片则检查是否真实图片,并检查扩展名
  • 检查文件大小是否超过限制,默认是 2M
  • 检查文件上传是否是 POST
  • 可检查文件的扩展名
  • 对文件名进行散列 md5 处理
  • 结合数据库可解决重复文件上传问题,若是重复则直接返回已经上传的路径(这个当前没提供,可通过 Hash 判断文件是否上传)
  1. // upload.php
  2. require_once __DIR__.DIRECTORY_SEPARATOR.'lib'.DIRECTORY_SEPARATOR.'helper.php';
  3. $files=upload();
  4. $res=[];
  5. foreach($files as $file){
  6. array_push($res,uploadFile($file,false));
  7. }
  8. var_dump($res);
  1. // lib/helper.php
  2. // 对客户端上传的单文件或多文件进行分析处理
  3. // 返回的数组是上传文件信息,若只有一个则只有一个成员,若是多个则多个成员
  4. function upload()
  5. {
  6. $files = [];
  7. foreach ($_FILES as $file) {
  8. foreach ($file['name'] as $k => $v) {
  9. $name = $v;
  10. $type = $file['type'][$k];
  11. $tmp_name = $file['tmp_name'][$k];
  12. $error = $file['error'][$k];
  13. $size = $file['size'][$k];
  14. array_push($files, compact('name', 'type', 'tmp_name', 'error', 'size'));
  15. }
  16. }
  17. return $files;
  18. }
  19. // 完成文件上传
  20. function uploadFile($file, $flag = true, $path = './upload', $ext = [], $maxSize = 2 * 1024 * 1024)
  21. {
  22. // 先处理内置错误
  23. switch ($file['error']):
  24. case 0:
  25. break;
  26. case 1:
  27. $res['msg'] = '文件大小超过PHP.ini中upload_max_filesize指定的值';
  28. break;
  29. case 2:
  30. $res['msg'] = '文件大小超过表单中MAX_FILE_SIZE指定的值';
  31. break;
  32. case 3:
  33. $res['msg'] = '文件只有部分被上传';
  34. break;
  35. case 4:
  36. $res['msg'] = '没有文件被上传';
  37. break;
  38. case 6:
  39. $res['msg'] = '找不到临时文件夹';
  40. break;
  41. case 7:
  42. $res['msg'] = '文件写入失败';
  43. break;
  44. default:
  45. $res['msg'] = '系统错误';
  46. endswitch;
  47. // 处理后端自定义错误
  48. // 获取文件扩展名
  49. $extFile = substr($file['name'], strrpos($file['name'], '.') + 1);
  50. // 若是图片则判断是否真实图片和扩展名
  51. if ($flag) {
  52. $extImg = ['jpg', 'jpeg', 'png', 'wbmp', 'gif'];
  53. // 若是图片则返回数组,否则是false
  54. if (!getimagesize($file['tmp_name']))
  55. $res['msg'] = '不是合法的图片';
  56. if (!in_array($extFile, $extImg))
  57. $res['msg'] = '非法图片类型';
  58. }
  59. // 判断大小是否超过限制2M
  60. if ($file['size'] > $maxSize)
  61. $res['msg'] = '文件大小超过2M,请确保文件小于2M';
  62. // 判断是否POST上传
  63. if (!is_uploaded_file($file['tmp_name']))
  64. $res['msg'] = '文件不是通过HTTP POST上传';
  65. // 若设置扩展名,则检查文件类型
  66. if (count($ext) > 0) {
  67. if (!in_array($extFile, $ext))
  68. $res['msg'] = '非法文件类型';
  69. }
  70. if (!file_exists($path)) {
  71. if (!mkdir($path, 0777, true))
  72. $res['msg'] = '服务端禁止上传文件';
  73. chmod($path, 0777);
  74. }
  75. // 拦截用户定义的错误
  76. if ($res) return $res;
  77. // 成功则移动
  78. if ($file['error'] === 0) {
  79. $newPath = $path . DIRECTORY_SEPARATOR . md5(substr($file['name'], 0, strrpos($file['name'], '.'))) . '.' . $extFile;
  80. $res['msg'] = '上传文件失败';
  81. if (move_uploaded_file($file['tmp_name'], $newPath)) {
  82. $res['msg'] = '上传文件成功';
  83. $res['path'] = $newPath;
  84. }
  85. }
  86. return $res;
  87. }

2、多维数组的提取

在上例代码中对多文件上传时分析处理,我基本是按照老师方式遍历多维数组的方式来提取,在细节上改进了一些。其实对二维数组某列的获取,PHP 提供了array_column(),如对上传文件$_FILES,可以是如下形式,不过唯一不足就是每个上传文件信息变成索引数组了,不过和上面关联数组是一一对应的,不影响读取。

  1. function upload()
  2. {
  3. $files = [];
  4. foreach ($_FILES as $file) {
  5. foreach ($file['name'] as $k => $v) {
  6. array_push($files, array_column($file,$k));
  7. }
  8. }
  9. return $files;
  10. }

3、对上传文件状态检查

这点我和老师基本思路是一样的,就是先检查内部定义的错误,后处理用户自定义的检查(包括是否真实图片、大小、上传方式和扩展名等),若有错误则返回,没有则从临时文件夹移动到指定的位置,并返回所有上传文件的状态信息。在一些细节方面进行了改进。

  • 对文件名的获取,老师只考虑普通的情况,可能文件名是page_new_23.my.img.jpg时,有多个点时,老师使用array_shift()获取文件名就不完整,我的解决方案是(substr($file['name'], 0, strrpos($file['name'], '.')),可避免老师提取文件名不全的问题

  • 对内部定义的错误的处理,我是当error为0时,直接break跳出switch的分支语句。

  • 对扩展名的检查不再局限于图片,毕竟上传不仅仅是图片,其它文件也需要检查扩展名,通过判断是否定义扩展名来决定是否检查扩展名。

  • 我的现在项目中对上传文件还会检查是否已经上传,是通过Hash文件来判断,可在数据库中保存上传文件的Hash信息,每次上传会进行判断,若已经上传则直接返回已经上传地址。这里我就不实现了,思路已经提供了,这里只是演示文件上传功能。

声明:本文内容转载自脚本之家,由网友自发贡献,版权归原作者所有,如您发现涉嫌抄袭侵权,请联系admin@php.cn 核实处理。
全部评论
文明上网理性发言,请遵守新闻评论服务协议