HTML5+PHP 实现 保存文件夹相对路径 递归上传 在线浏览

前端用了MetroUI,后台是ThinkPHP,数据库MySQL,先看看效果吧。由于项目涉及敏感词汇我就码了一下。

1.选择要上传的文件夹,上传以后默认都在根目录下。

HTML5+PHP 实现 保存文件夹相对路径 递归上传 在线浏览

2.看看后台管理界面的效果,实现多级目录,可以显示图片内容,返回上一级

HTML5+PHP 实现 保存文件夹相对路径 递归上传 在线浏览HTML5+PHP 实现 保存文件夹相对路径 递归上传 在线浏览HTML5+PHP 实现 保存文件夹相对路径 递归上传 在线浏览


正文:

谈到文件夹上传,应该都不觉得难,一个input框加上一个PHP后台就够了。但是这次的需求说起来容易,但是其实还挺难的。要把一个文件夹的文件递归上传,保存目录结构,能够在浏览器里展示出来,其实是三个过程。

【1】上传时要保存文件的相对路径。这里主要有两个问题。第一是怎样获取到路径,第二是用什么方式传到服务器。

【2】后台接受并且构成出目录结构。这里也是两个问题。第一,目录结构要怎么从前台post来的数据中分离出来。第二,用什么样的结构去保存,用什么样的逻辑去存储。其实这就是后台的“算法”了。

【3】让前台显示文件目录。首先考虑到如果文件很多,你不能一次性从数据库中全都select出来。然后我希望代码尽量优雅,最好和后台交互一个函数就搞定。


解决方案:

最最重要,先确定开发的浏览器环境,这里选择Chrome,2016年6月以后的版本都算比较新,没有一一去试,chrome保证功能无误,且可以移动端联调。HTML5 chrome移动设备和电脑端联调


【1】

首先定义一个能够承载文件夹的标签。webkitdirectory是这里的大杀器,这个不仅定义了文件夹上传,而且记录了文件的相对路径。

  1. <input type="file" name="multi_upload[]" id="multi_upload" multiple webkitdirectory />  

文件本身的上传,是用到了ThinkPHP的upload类,一旦你把input的files信息post给后台,只要这样几行代码就能指定目录保存了。

  1. $upload = new \Think\Upload();// 实例化上传类  
  2.         $upload->maxSize   =     3145728 ;// 设置附件上传大小  
  3.         $upload->exts      =     array('jpg''gif''png''jpeg');// 设置附件上传类型  
  4.         $upload->rootPath  =     './Public/octdatabase/'// 设置附件上传根目录  
  5.         $upload->savePath  =     ''// 设置附件上传(子)目录  
  6.   
  7.   
  8.         $targetFolder = '/Public/octdatabase/'// Relative to the root  
  9.   
  10.         $upload->saveName = array('myFun',array('__FILE__'));  
  11.         $upload->autoSub = false;  
  12.         // 上传文件   
  13.         $info = $upload->upload();  

相对路径又如何传给后台呢?相对路径没有包含在files的里面,需要单独挖出来上传。这里有一些小trick,比如files的最后两个其实不是图片文件本身,XMLHttpRequest是做了一个类似Ajax的交互,页面不会跳转,所以我把Ajax返回的内容显示在一个div标签里,这样就可以在网页中运行。 这部分代码主体参考了Alan的Blog:http://sapphion.com/2012/06/12/keep-directory-structure-when-uploading/ 。
[javascript] view plain copy
  1. $("#btn").click(function(){  
  2.         uploadFiles($("#multi_upload")[0].files);  
  3.     });  
  4.     function uploadFiles(files){  
  5.         // Create a new HTTP requests, Form data item (data we will send to the server) and an empty string for the file paths.  
  6.         xhr = new XMLHttpRequest();  
  7.         data = new FormData();  
  8.         var paths = new Array();  
  9.           
  10.         // Set how to handle the response text from the server  
  11.           
  12.         xhr.onreadystatechange = function(ev){  
  13.             if (xhr.readyState == 4){  
  14.                 $("#info").html(xhr.responseText);  
  15.             }  
  16.         };  
  17.           
  18.         // Loop through the file list  
  19.         for (var i in files){  
  20.             if (typeof files[i] != 'object'){  
  21.                 continue;  
  22.             }  
  23.             // Append the current file path to the paths variable (delimited by tripple hash signs - ###)  
  24.             paths.push(files[i].webkitRelativePath);  
  25.             //paths += files[i].webkitRelativePath+"###";  
  26.             // Append current file to our FormData with the index of i  
  27.             data.append(i, files[i]);  
  28.         };  
  29.         // Append the paths variable to our FormData to be sent to the server  
  30.         // Currently, As far as I know, HTTP requests do not natively carry the path data  
  31.         // So we must add it to the request manually.  
  32.         data.append('paths', paths);  
  33.               
  34.         // Open and send HHTP requests to upload.php  
  35.         xhr.open('POST'"/bgidb/Data/multiuploadify"true);  
  36.         xhr.send(this.data);  
  37.     }  

【2】已完成:文件保存到了后台,文件相对路径传给了后台。现在处理将文件相对路径剖开成文件夹的树形结构。树形结构中每个非根节点都有一个父节点,每个叶子结点都没有孩子。所以只要记录下来每个节点的id,和父亲的id,就能存储和读取一个树形结构了。

当然这里面依旧有trick。首先考虑一下你的文件相对路径长什么样子,如这样:“images/folder1/folder2/xxx.jpg”。当然你可以dump到前台自己看看。“/”符号将每个节点隔开了,所以只要把所有这样的相对路径分离开,你就得到了每一个文件夹(文件)的名字,每一个文件夹(文件)的父亲。explode函数能够完成隔开节点,以及生成节点的功能。接下来就是一个节点一个节点地把树形结构写到数据库。

  1. protected function insertFolderInfo(){  
  2.         //插入文件夹层级信息  
  3.         $reletivePath = explode(',',I('post.paths'));//把传到后台的paths变成数组  
  4.   
  5.         $flag = true;  
  6.   
  7.         foreach ($reletivePath as $key => $path) {  
  8.             $pathTree = explode('/',$path);  
  9.             $insertId = 0;  
  10.             $octFolder = M("octfolder");  
  11.   
  12.             foreach ($pathTree as $key => $nodeName) {  
  13.                 if ($key == 0){//舍去最高层目录  
  14.                     continue;  
  15.                 }  
  16.   
  17.                 $leaf = 0;  
  18.                 if ($key == sizeof($pathTree) - 1){  
  19.                     $leaf = 1;  
  20.                 }  
  21.                 $nodeAdd = array(  
  22.                     'nodeName'  => $nodeName,  
  23.                     'parent'    => $insertId,  
  24.                     'leaf'      => $leaf  
  25.                 );  
  26.                 $re = $octFolder->where($nodeAdd)->find();  
  27.   
  28.                 if (!$re){  
  29.                     $insertId = $octFolder->add($nodeAdd);  
  30.   
  31.                     if (!$insertId){  
  32.                         $flag = false;  
  33.                         break;  
  34.                     }  
  35.                 }  
  36.                 else{  
  37.                     $insertId = $re['id'];  
  38.                 }  
  39.             }  
  40.             if ($flag == false){  
  41.                 break;  
  42.             }  
  43.         }  
  44.         return $flag;  
  45.     }  
数据库表结构:

HTML5+PHP 实现 保存文件夹相对路径 递归上传 在线浏览




【3】树形结构也保存到了数据库。接下来从前台优雅的把它取出来。当看目录里的文件时,当前目录里的一级文件夹(文件)的父节点都是一样的,都是当前目录的id。所有只要定义一个query函数,每次显示一个id的所有孩子,事情就简单了。剩下的就是页面的更新,触发器的绑定(为了更好的操作体验)。

[javascript] view plain copy
  1. function query(id){  
  2.                 $.post(  
  3.                     '/bgidb/oct/octFileAjax',  
  4.                     {parent : id},  
  5.                     function(data){  
  6.                         //console.log(data);  
  7.                         if(data['wrongcode']==999){  
  8.                             backStepId = data['backStepId'];  
  9.                             var files = data['files'];  
  10.                             filePanel.html(formatFiles(files));  
  11.                             boundListener();  
  12.                         }else{  
  13.                             alert(data['wrongmsg']);  
  14.                         }  
  15.                     },  
  16.                     "json");  
  17.             }  

后台只要根据这个parent参数去找到所有的子节点,然后传给前台,是图片的附个图片链接,然后还附上上一级目录的id以便回退就ok。前台接到了data之后,就把前端的文件夹都画出来,触发器绑好。

  1. public function octFileAjax(){  
  2.         $WRONG_CODE = C('WRONG_CODE');  
  3.         $WRONG_MSG = C('WRONG_MSG');  
  4.         $data['wrongcode'] = $WRONG_CODE['totally_right'];  
  5.   
  6.         $parent = I("post.parent", null);  
  7.         if ($parent == null){  
  8.             $data['wrongcode'] = $WRONG_CODE['query_data_invalid'];  
  9.         }  
  10.         else{  
  11.             $oF = M("octfolder");  
  12.             $cond = array(  
  13.                 'parent' => $parent  
  14.             );  
  15.             $re = $oF->where($cond)->select();  
  16.   
  17.             if (!$re){  
  18.                 $data['wrongcode'] = $WRONG_CODE['not_exist'];  
  19.             }  
  20.             else{  
  21.                 foreach ($re as $key => $file) {  
  22.                     if($file['leaf'] == 1){  
  23.                         $oct = M("oct");  
  24.                         $cond = array(  
  25.                             'name' => $file['nodename']  
  26.                         );  
  27.                         $img = $oct->field('imgsite')->where($cond)->find();  
  28.                         $re[$key]['imgsite'] = $img['imgsite'];  
  29.                     }  
  30.                 }  
  31.   
  32.                 $data['files'] = $re;  
  33.   
  34.                 $oF = M("octfolder");  
  35.                 $cond = array(  
  36.                     'id' => $parent  
  37.                 );  
  38.                 $backStepId = $oF->field('parent')->where($cond)->find();  
  39.                 if (!$backStepId){  
  40.                     $data['backStepId'] = 0;  
  41.                 }  
  42.                 else{  
  43.                     $data['backStepId'] = $backStepId['parent'];  
  44.                 }  
  45.             }  
  46.         }  
  47.   
  48.         $data['wrongmsg'] = $WRONG_MSG[$data['wrongcode']];  
  49.         $this->ajaxReturn($data);  
  50.     }  


完整浏览页面前端实现:这里实在不想自己画文件夹的前端了,就用了MetroUI里的图标和样式。

HTML5+PHP 实现 保存文件夹相对路径 递归上传 在线浏览

我自己完整实现:

  1. <!DOCTYPE html>  
  2. <html>  
  3. <head>  
  4.     <title>OCT文件浏览系统</title>  
  5.   
  6.     <link href="__PUBLIC__/metro/css/metro.min.css" rel="stylesheet">  
  7.   
  8.       
  9.     <script src="__PUBLIC__/js/jquery-2.1.1.min.js" type="text/javascript"></script>  
  10.     <script src="__PUBLIC__/metro/js/metro.min.js" type="text/javascript"></script>  
  11.     <script type="text/javascript">  
  12.         $(document).ready(function(){  
  13.             var filePanel = $("#filePanel");  
  14.             var backStepId = 0;  
  15.   
  16.             query(0);  
  17.   
  18.             function query(id){  
  19.                 $.post(  
  20.                     '/bgidb/oct/octFileAjax',  
  21.                     {parent : id},  
  22.                     function(data){  
  23.                         //console.log(data);  
  24.                         if(data['wrongcode']==999){  
  25.                             backStepId = data['backStepId'];  
  26.                             var files = data['files'];  
  27.                             filePanel.html(formatFiles(files));  
  28.                             boundListener();  
  29.                         }else{  
  30.                             alert(data['wrongmsg']);  
  31.                         }  
  32.                     },  
  33.                     "json");  
  34.             }  
  35.   
  36.             function formatFolder(file){  
  37.                 //backStepId = file.parent;  
  38.                 return '<div class="list octFolder" folderId="' + file.id + '" folderParentId="' + file.parent + '" ><img src="/Public/metro/images/folder-images.png" class="list-icon"><span class="list-title">'+ file.nodename +'</span></div>';  
  39.             }  
  40.   
  41.             function formatImg(file){  
  42.                 return '<div class="list octImage" name="' + file.nodename + '"><img src="' + file.imgsite + '" class="list-icon"><span class="list-title">'+ file.nodename +'</span></div>';  
  43.             }  
  44.   
  45.             function formatStepBack(){  
  46.                 return  '<div class="list stepBack"><span class="list-icon icon-font-icon">..</span><span class="list-title">上级目录</span></div>';  
  47.             }  
  48.   
  49.             function formatFiles(files){  
  50.   
  51.   
  52.                 var re = "";  
  53.   
  54.                 re += formatStepBack(files);  
  55.   
  56.                 for(var i = 0; i < files.length; i++){  
  57.                     var file = files[i];  
  58.                     if(file.leaf == 0){  
  59.                         re += formatFolder(file);  
  60.                     }  
  61.                     else{  
  62.                         re += formatImg(file);  
  63.                     }  
  64.                 }  
  65.                 return re;  
  66.             }  
  67.   
  68.             function boundListener(){  
  69.                 $(".octImage").click(function(){  
  70.                     var nodeName = $(this).attr('name');  
  71.                     $("#input").val(nodeName);  
  72.                     $("#uploadAgent").submit();  
  73.                 })  
  74.   
  75.                 $(".octFolder").click(function(){  
  76.                     var folderId = $(this).attr('folderId');  
  77.                     query(folderId);  
  78.                 })  
  79.   
  80.                 $(".stepBack").click(function(){  
  81.                     query(backStepId);  
  82.                 })  
  83.             }  
  84.         });  
  85.   
  86.   
  87.     </script>  
  88. </head>  
  89. <body>  
  90.     <div class="app-bar">  
  91.         <a class="app-bar-element" href="__MODULE__/oct/octFileSys">OCT文件浏览系统</a>  
  92.         <span class="app-bar-divider"></span>  
  93.         <a class="app-bar-element" href="/">回到首页</a>  
  94.     </div>  
  95.     <form action="{:U('Bgidb/Oct/oct')}" enctype="multipart/form-data" method="post" id="uploadAgent" target='_blank' ">  
  96.         <input type="hidden" name="imageName" id="input"/>  
  97.     </form>  
  98.   
  99.     <div class="listview " id="filePanel">  
  100.     </div>  
  101. </body>  
  102. </html>  



这个其实是一个很大的系统的一部分,其它部分也都是我一直维护的,快两年了。

最后自己对于安全方面没有做工作,因为我对这个领域没有概念,我只是在理解力范围内随手减少代码的臃肿。