服务器基于PHP CodeIgniter,Android基于Volley实现多文件/图片上传(含服务器,web版和android客户端完整代码)

问题背景:app在上传图片时,同时传递参数,支持传递多个图片。本文中的环境默认已经配好了服务器的CodeIgniter框架。事实上不使用这个框架也是可以的。

一,服务器部分

1,在controllers下的helpers新建文件upload_helper.php


<?php
/**
 * Make multifile array input complaint with CI_Upload.<br>
 * For use files[ ] input name you must use it method.
 *
 * @author porquero
 *
 * @example
 * In Controller<br>
 * $this->load->helper('upload');<br>
 * multifile_array();<br>
 * foreach ($_FILES as $file => $file_data) {<br>
 *    $this->upload->do_upload($file);
 * ...
 *
 * @link http://porquero.blogspot.com/2012/05/codeigniter-multifilearray-upload.html
 */
function multifile_array()
{
    if(count($_FILES) == 0)
        return;

    $files = array();
    $all_files = $_FILES['f_file']['name'];
    $i = 0;

    foreach ((array)$all_files as $filename) {
        $files[++$i]['name'] = $filename;
        $files[$i]['type'] = current($_FILES['f_file']['type']);
        next($_FILES['f_file']['type']);
        $files[$i]['tmp_name'] = current($_FILES['f_file']['tmp_name']);
        next($_FILES['f_file']['tmp_name']);
        $files[$i]['error'] = current($_FILES['f_file']['error']);
        next($_FILES['f_file']['error']);
        $files[$i]['size'] = current($_FILES['f_file']['size']);
        next($_FILES['f_file']['size']);
    }
    $_FILES = $files;
}

说明:

a.注意里面的key为'f_file',这就要求app或web在上传,建表单的时候将此值对应上。

b.该文件主要是遍历$_FILES,通过current得到当前file的信息转存到数组里,然后返回。注意转存后索引是从1开始的。转存的字段有name/type/tmp_name/error/size,使用next移动$_FILES['f_file']['key']的指针。

2.views里新建upload_form.php,用来在web上模拟测试上传是否成功:

<html>
<head>
    <title>Upload Form</title>
</head>
<body>

<?php echo $error;?>
<?php $data = array('type'=>'shop', 'id'=>'1');?>

<?php echo form_open_multipart('upload/web_upload', '', $data);?>

<input type="file" name="f_file[]" multiple="multiple" size="20" />

<br /><br />

<input type="submit" value="upload" />

</form>

</body>
</html>
注意:

a,这里使用了CI框架的form_open_multipart新建一个multipart的表单,访问的是控制器upload里的web_upload方法,第三个参数$data,用于模拟向服务器传递的post请求参数。当然你也可以在下面加几个<input>.

b,input里name对应的是f_file[],这个是跟 服务器那边统一好的。

c,若要支持多文件上传加上multiple="multiple",不加的话一次只能上传一个文件。


3,views下新建upload_success.php,显示上传成功后的界面。

<html>
<head>
    <title>Upload Form</title>
</head>
<body>

<h3>Your file was successfully uploaded!</h3>

<ul>
    <?php foreach ($upload_data as $item => $value):?>
        <li><?php echo $item;?>: <?php echo $value;?></li>
    <?php endforeach; ?>
</ul>

<p><?php echo anchor('upload', 'Upload Another File!'); ?></p>

</body>
</html>

注意:这里的$upload_data是控制器上传成功后传给view的数据。


4,接下来是最关键的一个类,在controllers文件夹下新建Upload.php,这是个控制器,上传最核心的。

<?php

require_once APPPATH . 'controllers/base/BASE_CI_Controller.php';
class Upload extends BASE_CI_Controller{
    private $m_type = '';
    private $m_id = '';
    private $m_path = './application/cache/image';
    private $m_error = array();
    public function __construct()
    {
        parent::__construct();
        $this->load->helper(array('form', 'url', 'upload'));
        //$this->load->model('picture_model');
    }

    public function index()
    {
        $this->load->view('upload_form', array('error' => ' ' ));
    }

    public function app_upload(){
        $this->init_argc();
        multifile_array();
        foreach ($_FILES as $file => $file_data){
            $this->do_upload($file);
        }
        if($this->m_error == NULL || count($this->m_error) == 0){
            $this->output->set_output(json_encode(array('msg'=>'上传成功')));
        }else{
            $this->output->set_output(json_encode($this->m_error));
        }
    }


    public function do_upload($file)
    {
        $config['upload_path']      =  $this->m_path;
        $config['allowed_types']    = 'gif|jpg|png|jpeg';
        $config['max_size']     = 10240;
        $config['max_width']        = 2000;
        $config['max_height']       = 2000;
        $config['file_name'] = Util::random_str();

        $this->load->library('upload', $config);

        if ( ! $this->upload->do_upload($file)){
            $this->on_upload_error($this->upload->display_errors());
        }
        else{
            $upload_data = $this->upload->data();
            $this->on_upload_success($upload_data['file_name']);
        }
    }

    public function do_upload2($file)
    {
        $config['upload_path']      =  $this->m_path;
        $config['allowed_types']    = 'gif|jpg|png|jpeg';
        $config['max_size']     = 10240;
        $config['max_width']        = 2000;
        $config['max_height']       = 2000;
        $config['file_name'] = Util::random_str();

        $this->load->library('upload', $config);

        if ( ! $this->upload->do_upload($file))
        {
            $error = array('error' => $this->upload->display_errors());
            $this->load->view('upload_form', $error);
        }
        else
        {
            $data = array('upload_data' => $this->upload->data());

            $this->load->view('upload_success', $data);
        }
    }

    public function web_upload()
    {
        multifile_array();
        foreach ($_FILES as $file => $file_data){
            $this->do_upload2($file);
        }
    }

    private function init_argc() {
        $this->m_type = $this->getPost('type');
        $this->m_id = $this->getPost('id');
        $this->m_path = $this->getPath($this->m_type);
    }

    private function getPath($type){
        $path = './application/cache/image/shop';
        if($type == "shop"){
            $path =  './application/cache/image/shop';
        }
        return $path;

    }

    private function on_upload_success($name){
        if($this->m_type == 'shop'){
            //$this->picture_model->add_shop_picture($this->m_id, $this->m_type, $name);
        }else if($this->m_type == 'avatar'){
            //$this->picture_model->add_user_avatar($this->m_id, $this->m_type, $name);
        }
    }
    private function on_upload_error($error){
        $this->m_error['msg'] = $error;
    }

}
?>
解释如下:

a,这里Upload是继承的BASE_CI_Controller,也可以换成CI_Controller,在自己的Base_CI_Controller里封装了自己项目一些常用的安全校验逻辑;

b,我定义了m_type记录上传图片的类型,m_id是图片所属对象的id,m_path为路径,根据type不同路径可以做区分。m_error纪录错误。在构造函数里,注意把几个helper都load进来。除此外我还写了个Picture_model用来操作图片相关的数据库,如果不需要这个model,可以注释掉。

c,app_load()是暴露给app用来上传的,init_argc()初始化post传来的各种参数。然后就是调multifile_array();之后遍历上传。待上传完毕后根据m_error里是否为空,判断是该显示什么消息给app。在do_upload()里的Util::random_str()是个很简单的对时间戳md5,防止图片名字一样:

Util里的代码:

    /**
     * 产生新的token
     * @return string
     */
    public static function token(){
        $curr = Util::time();
        return md5($curr);
    }

    public static function random_str(){
        return Util::token();
    }

每次上传成功后都调on_upload_success() on_upload_error()进行更新数据库等操作。其中on_upload_success()要接收$upload_data['file_name']),表示上传成功后的文件的名字。

d,web_upload是给web上传图片用的,通过do_upload2()上传成功后就加载一个view来显示上传后的信息。PS:保证你对目的文件夹有可写权限。

先用web测试下效果:http://localhost/~yanzi/city52/index.php/upload

服务器基于PHP CodeIgniter,Android基于Volley实现多文件/图片上传(含服务器,web版和android客户端完整代码)_第1张图片

二,客户端:基于Volley的多文件/图片上传类的封装

这个比较简单,基于volley封装的,MultipartRequest.java

package com.android.nomnom.volley;

import android.util.Log;

import com.android.volley.AuthFailureError;
import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyLog;
import com.android.volley.toolbox.HttpHeaderParser;

import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 功能:
 *
 * @author yanzi E-mail: [email protected]
 * @version 创建时间: 2015-08-09 下午4:32
 */
public class MultipartRequest extends Request<String>{

    private MultipartEntity entity = new MultipartEntity();

    private  Response.Listener<String> mListener;

    private List<File> mFileParts;
    private String mFilePartName;
    private Map<String, String> mParams;

    /**
     * 单个文件+参数 上传
     * @param url
     * @param listener
     * @param errorListener
     * @param filePartName
     * @param file
     * @param params
     */
    public MultipartRequest(String url, Response.Listener<String> listener, Response.ErrorListener errorListener,
                            String filePartName, File file, Map<String, String> params){
        super(Method.POST, url, errorListener);
        mFileParts = new ArrayList<File>();
        if(file != null && file.exists()){
            mFileParts.add(file);
        }else{
            VolleyLog.e("MultipartRequest---file not found");
        }
        mFilePartName = filePartName;
        mListener = listener;
        mParams = params;
        buildMultipartEntity();

    }

    /**
     * 多个文件+参数上传
     * @param url
     * @param listener
     * @param errorListener
     * @param filePartName
     * @param files
     * @param params
     */
    public MultipartRequest(String url,Response.Listener<String> listener,Response.ErrorListener errorListener
                            , String filePartName,List<File> files, Map<String, String> params) {
        super(Method.POST, url, errorListener);
        mFilePartName = filePartName;
        mListener = listener;
        mFileParts = files;
        mParams = params;
        buildMultipartEntity();
    }

    @Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        String parsed;
        try {
            parsed = new String(response.data,
                    HttpHeaderParser.parseCharset(response.headers));
        } catch (UnsupportedEncodingException e) {
            parsed = new String(response.data);
        }
        return Response.success(parsed,
                HttpHeaderParser.parseCacheHeaders(response));
    }

    @Override
    protected void deliverResponse(String response) {
        mListener.onResponse(response);
    }

    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        Map<String, String> headers = super.getHeaders();

        if (headers == null || headers.equals(Collections.emptyMap())) {
            headers = new HashMap<String, String>();
        }
        return headers;
    }

    @Override
    public String getBodyContentType() {
        return entity.getContentType().getValue();
    }

    @Override
    public byte[] getBody() throws AuthFailureError {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try{
            entity.writeTo(bos);
        } catch (IOException e) {
            VolleyLog.e("IOException writing to ByteArrayOutputStream");
        }
        return bos.toByteArray();
    }

    private void buildMultipartEntity() {
        if (mFileParts != null && mFileParts.size() > 0) {
            for (File file : mFileParts) {
                entity.addPart(mFilePartName, new FileBody(file));
            }
            long l = entity.getContentLength();
            Log.i("YanZi-volley", mFileParts.size() + "个,长度:" + l);
        }

        try {
            if (mParams != null && mParams.size() > 0) {
                for (Map.Entry<String, String> entry : mParams.entrySet()) {
                    entity.addPart(
                            entry.getKey(),
                            new StringBody(entry.getValue(), Charset
                                    .forName("UTF-8")));
                }
            }
        } catch (UnsupportedEncodingException e) {
            VolleyLog.e("UnsupportedEncodingException");
        }
    }
}

使用的话new一个request,然后add到queue里就可以了,我就不写示例了。下次介绍android上传/下载文件带进度条的实现。

参考:CodeIgniter http://codeigniter.org.cn/

---------------本文系原创,转载注明作者yanzi1225627


2015-12-30补充说明:android端的测试demo我已经传到了git,https://github.com/yanzi1225627/TestMultipartRequest,欢迎大家fork。友情提示那个key要写 f_file[],不要写f_file.


欢迎大家加入PHP CodeIgniter社区群460132647,备注yanzi





你可能感兴趣的:(PHP,android,CodeIgniter,图片上传)