jquery ajax put file, php save file

在项目中第一次遇到这个问题,当我在更新用户头像的时候,选择使用put上传头像,然而服务端并不能通过$_FILES像我们以前一样得到想要的结果。这是一个相当复杂的问题,我们这篇文章就来试图解决这个问题。

restful post vs. put

如果我们希望严格保持restful风格,那么一定要遵循post=add, put=update的风格,在stackoverfollow上已经有很多这样的讨论,所以,就不这里再讨论。但是restful不是规范,我们并不一定非得严格按照这个规定去做,在适当的时候,其实我们确实可以选择使用post代替put.

为什么仍然坚持put

在PHP接受数据传递时,只有$_GET, $_POST,而$_PUT和$_DELETE是没有的,当然,由于delete操作往往把要删除的资源直接在url中加以确定。所以,现在其实最麻烦的,是put操作。

我们有足够的理由使用put,因为我们在接受put信息时,采用的是php://input的形式,这也就是说,通过

file_get_contents('php://input')

可以获取传递内容的原始信息,以及二进制流,这使得传输和接受都是非常高效率的。我们甚至可以直接通过put传递文件的二进制流,在服务端保存这个二进制信息,就得到了原始的文件。这个过程不会在服务器上产生临时文件,不会消耗更多的内存。

如何获取put信息

但是,随之而来的是,解析put信息成为让人非常烦恼的事,我们通过网上(官网)的一些示例代码,发现也只能解决部分问题,比如当我们通过发送put信息到服务端更新文章信息时,由于文章信息不包含复杂信息,服务端通过官方示例就可以得到对应的信息,而这样的到的信息,和$_POST没有什么不同。

然而,如果发送的信息中如果含有文件,则情况将会大不一样。

在$_POST文件时,要求表单含有enctype='multipart/form-data'属性,而以往我们长长不去思考它的作用,现在我们才知道,这其实时一中表单请求的加密形式,当表单含有该属性,提交的内容中含有文件时,服务器将会按照该规则进行解码,而解码的到的文件信息,将会以$_FILES返回。所以,当我们在使用post进行文件上传的时候,实际上,文件不能到$_POST中去取,而是到$_FILES中。

put传输的是二进制信息流,因此不需要服务端解码,自然也就不存在$_FILES的具体信息(为空)。这下我们就懵了,那上传的文件到哪里去了(我们知道$_FILES中的文件保存在服务器临时目录中,需要我们通过move_uploaded_file函数进行转移)?

实际上,put的文件哪里都没有去,还在php://input原始信息流中,我们要做的,是对php://input进行解码,得到我们想要的文件信息。

我们来看put得到的结果:

echo file_get_contents('php://input');
exit;

一般而言,我们可以得到两种结果,

一种:不包含文件的

这时我们长长得到的,是一串字符串,例如name=Jerry&age=18,我们在大部分PHP中如此去解析:

parse_str(file_get_contents('php://input'), $_PUT);

另一种:包含文件的

这种情况是比较复杂的,我们会发现,php://input是一个复杂的体系。

------------------------------b2449e94a11c
Content-Disposition: form-data; name="user_id"

3
------------------------------b2449e94a11c
Content-Disposition: form-data; name="post_id"

5
------------------------------b2449e94a11c
Content-Disposition: form-data; name="image"; filename="/tmp/current_file"
Content-Type: application/octet-stream

�����JFIF���������... a bunch of binary data

可以看到上面的结果中,用------------------------------b2449e94a11c把整个内容分为了几个部分,其中第三个部分是一个文件。

经过观察,我们最终决定,用正则分析它,并得到我们想要的结果,最后我参考一些网上的朋友给出的代码,总结了下面这个函数:

/**
 * Parse raw HTTP request data
 *
 * Pass in $a_data as an array. This is done by reference to avoid copying
 * the data around too much.
 *
 * Any files found in the request will be added by their field name to the
 * $data['files'] array.
 *
 * http://www.tangshuang.net/?p=2294
 *
 * @param   array  Empty array to fill with data
 * @return  array  Associative array of request data
 */
function parse_http_input_raw() {
    $a_data = array();

    // read incoming data
    $input = file_get_contents('php://input');

    // grab multipart boundary from content type header
    preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches);

    // content type is probably regular form-encoded
    if (!count($matches))
    {
        // we expect regular puts to containt a query string containing data
        parse_str(urldecode($input), $a_data);
        return $a_data;
    }

    $boundary = $matches[1];

    // split content by boundary and get rid of last -- element
    $a_blocks = preg_split("/-+$boundary/", $input);
    array_pop($a_blocks);


    // loop data blocks
    foreach ($a_blocks as $id => $block)
    {
        if (empty($block))
            continue;

        // you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char

        // parse uploaded files
        if (strpos($block, 'filename=') !== FALSE)
        {
            // match "name", then everything after "stream" (optional) except for prepending newlines
            preg_match("/name=\"([^\"]*)\".*filename=\"([^\"].*?)\".*Content-Type:\s+(.*?)[\n|\r|\r\n]+([^\n\r].*)?$/s", $block, $matches);
            $a_data['files'][$matches[1]] = array(
                'name' => $matches[1],
                'filename' => $matches[2],
                'type' => $matches[3],
                'blob' => $matches[4]
            );
        }
        // parse all other fields
        else
        {
            // match "name" and optional value in between newline sequences
            preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches);

            print_r($matches);

            $a_data[$matches[1]] = $matches[2];
        }
    }
    return $a_data;
}

所有的文件,被放在一个字段中提供使用:

$_PUT = parse_http_input_raw();

注意,文件的二进制流还在$_PUT中呢。我们需要通过遍历$_PUT['files']保存文件。

jquery ajax如何去发起put请求和上传

这个是最简单的了,可以看我之前写过的一些文章,非常详细,这里仅给出核心代码:

$.ajax({
    url: url,
    type: 'PUT',
    data: data,
    success: function(result){}
});

仅此而已。但是,当我们需要上传文件或图片的时候,就相对复杂一些,因为无论是POST或PUT,ajax都不会主动把表单中的文件流上传,而是只提交其他类型的数据。

我们需要借助FormData()来实现文件的ajax上传:

$('#form').on('submit',function(e){
    e.preventDefault();
    var $form = $(this),
        $files = $form.find('input[type=file][name]'),
        data = new FormData();
    $files.each(function() {
        if ('files' in this && this.files.length > 0) {
            data.append(this.name, this.files[0]);
        }
    });
    // 省略了把非文件数据加入到data中
    
    $.ajax({
        url: 'your url',
        type: 'PUT',
        data: data,
        contentType: false,
        processData: false,
        success: function(result){}
    });
});

可以看到,这个过程也很简单,并没有想象到那么复杂。通过new FormData()产生的对象,通过遍历form中的input或其他数据对象,将结果逐个加入到data中。new FormData()有一个append方法,可以实现append(key,value),从而将你需要的数据加入到data中,ajax提交到时候,data则是全部的有效数据(包括文件的二进制流)。

(完)

2016-03-26 |

已有2条评论
  1. 神的尾巴 2016-10-09 21:59

    我目前也是碰到这个问题,使用PUT上传文件的话,使用file_get_contents(‘php://input’)会获取到html from-data格式的串,用正则匹配能解决文件比较小的情况,但是如果文件比较大的直接加载进内存会不会比较消耗资源,另外如果字符串中有’——————————xxxx’格式的文本会不会影响解析?
    能不能有比较好的方案,能够模仿PHP处理POST请求一样,把文件流存到临时文件,并通过$_FILES来保存?

    • 否子戈 2016-10-12 18:38

      这只是一种思路,具体的情况还是要根据你自己的编程情况来确定。
      你说文件比较大会不会加载内存消耗资源,这个真可能有,在post上传的时候,你可以看到浏览器有一个进度条,在服务器上的临时文件也是逐渐保存为一个和本地相同的文件的,也就是说这种上传有流式的效果,起码在文件上传完之后不会再占用资源,但是put是直接把整个文件的二进制内容上传上去,所以是一次性的,内存肯定会占用到你主动释放为止。
      字符串中有与分割信息相同的文本就要自己做处理,这个并不涉及本文的内容。
      至于你最后说模仿post结果,我觉得如果没有必要,直接用post代替效率更高,你用put本来不就是为了解决post的问题吗?现在你却返回来要模仿post,或许有点简单问题复杂化了。