如何使用PHP通过HTTP PUT接收文件

32

有一件事情困扰了我一段时间.. 我正在构建一个RESTful API,需要在某些情况下接收文件。

使用HTTP POST时,我们可以从$_POST读取数据,从$_FILES读取文件。

使用HTTP GET时,我们可以从$_GET读取数据,从$_FILES读取文件。

然而,使用HTTP PUT时,据我所知,读取数据的唯一方法是使用php://input stream

一切都很好,但是当我想通过HTTP PUT发送文件时,php://input流就不再像预期的那样工作,因为它里面也有一个文件。

这是我目前如何读取PUT请求中的数据:

(只要没有文件被发布,这个方法就很好用)

$handle  = fopen('php://input', 'r');
$rawData = '';
while ($chunk = fread($handle, 1024)) {
    $rawData .= $chunk;
}

parse_str($rawData, $data);
当我输出rawData时,它显示为:
-----ZENDHTTPCLIENT-44cf242ea3173cfa0b97f80c68608c4c
Content-Disposition: form-data; name="image_01"; filename="lorem-ipsum.png"
Content-Type: image/png; charset=binary

�PNG
���...etc etc...
���,
-----ZENDHTTPCLIENT-8e4c65a6678d3ef287a07eb1da6a5380
Content-Disposition: form-data; name="testkey"

testvalue
-----ZENDHTTPCLIENT-8e4c65a6678d3ef287a07eb1da6a5380
Content-Disposition: form-data; name="otherkey"

othervalue

有人知道如何正确地通过HTTP PUT接收文件,或如何从php://input流中解析文件吗?

===== 更新 #1 =====

我已经尝试过以上方法,不太清楚还能做什么。

使用此方法时没有出现任何错误,除了我没有获得所需的发布数据和文件之外。

===== 更新 #2 =====

我正在使用Zend_Http_Client发送此测试请求,如下所示: (到目前为止,我还没有遇到任何Zend_Http_Client的问题)

$client = new Zend_Http_Client();
$client->setConfig(array(
    'strict'       => false,
    'maxredirects' => 0,
    'timeout'      => 30)
);
$client->setUri( 'http://...' );
$client->setMethod(Zend_Http_Client::PUT);
$client->setFileUpload( dirname(__FILE__) . '/files/lorem-ipsum.png', 'image_01');
$client->setParameterPost(array('testkey' => 'testvalue', 'otherkey' => 'othervalue');
$client->setHeaders(array(
    'api_key'    => '...',
    'identity'   => '...',
    'credential' => '...'
));

===== 解决方法 =====

结果我犯了一些错误的假设,主要是认为HTTP PUT与HTTP POST类似。正如下面所述,DaveRandom向我解释了HTTP PUT不适用于在同一请求中传输多个文件。

我现在已经将表单数据的传输从正文移到了URL查询字符串中。现在正文中保存单个文件的内容。

有关更多信息,请阅读DaveRandom的答案。它很棒。


1
嗯?当您尝试这样做时,会出现什么错误? - Naftali
你是如何发送请求的?php://input仅在多部分请求中无法工作... - Maxim Krizhanovsky
2
@Jack 不是使用PUT方法。 - DaveRandom
@Maurice 解析多部分消息并不难,但这并不是答案。答案是要使您的PUT请求一开始就有效,这样就不需要解析了。 - DaveRandom
@DaveRandom 为了确保我的基础知识正确,发送和接收PUT请求的正确方法是什么?表单数据应该放在哪里,文件内容应该放在哪里? - Maurice
显示剩余4条评论
5个回答

45
您所展示的数据不代表有效的PUT请求体(虽然它可能是,但我非常怀疑)。它展示了一个multipart/form-data请求体——这是在通过HTML表单通过HTTP POST上传文件时使用的MIME类型。
PUT请求应该完全配合GET请求的响应——它们将文件内容发送到消息体中,仅此而已。
基本上我想说的是接收文件的代码没有问题,出错的是发出请求的代码——客户端代码不正确,而不是您在此处展示的代码(尽管parse_str()调用是无意义的练习)。
如果您解释一下客户端是什么(浏览器、其他服务器上的脚本等),那么我可以帮您更进一步。就目前而言,您展示的请求体适合POST请求方法,而不是PUT。
让我们先暂停一下,看一下HTTP协议的总体情况 - 尤其是客户端请求方面 - 希望这能帮助你理解所有这些应该如何工作。首先,讲一下一点历史(如果您对此不感兴趣,可以跳过本节)。
历史
HTTP最初被设计为从远程服务器检索HTML文档的机制。起初,它只有效地支持GET方法,其中客户端将通过名称请求文档,服务器将返回给客户端。 HTTP的第一个公共规范标记为HTTP 0.9,于1991年发布 - 如果你感兴趣,可以在这里阅读here
HTTP 1.0规范(在1996年正式发布,RFC 1945)大大扩展了协议的功能,添加了HEAD和POST方法。由于响应格式的更改 - 添加了响应代码以及在MIME格式头中包含返回文档的元数据的能力 - 键/值数据对,因此它与HTTP 0.9不兼容。HTTP 1.0还将协议从HTML中抽象出来,允许以其他格式传输文件和数据。
HTTP 1.1是几乎完全使用的协议形式,建立在HTTP 1.0之上,并设计为向后兼容HTTP 1.0实现。它在1999年通过RFC 2616标准化。如果您是使用HTTP的开发人员,请了解此文档-它是您的圣经。充分理解它将使您比不了解它的同行具有相当的优势。
快点进入主题吧。
HTTP工作在请求-响应架构上 - 客户端向服务器发送请求消息,服务器向客户端返回响应消息。请求消息包括一个方法、一个URI和可选的一些头部。请求方法是这个问题所涉及的内容,因此我将在这里重点介绍它 - 但首先了解我们谈论请求URI时确切的含义非常重要。URI是我们请求的资源在服务器上的位置。通常,它由路径组件和可选的查询字符串组成。还有其他情况下可能存在其他组件,但为了简单起见,我们现在将忽略它们。假设你在浏览器地址栏中输入http://server.domain.tld/path/to/document.ext?key=value。浏览器拆分此字符串,并确定需要连接到位于server.domain.tld的HTTP服务器,并请求/document.ext?key=value文档。生成的HTTP 1.1请求至少如下:
GET /path/to/document.ext?key=value HTTP/1.1
Host: server.domain.tld

第一部分是单词GET - 这是请求方法。下一部分是我们请求的文件路径 - 这是请求URI。在第一行的末尾,有一个标识符表示正在使用的协议版本。在接下来的一行中,您可以看到一个名为Host的MIME格式标题。 HTTP 1.1要求每个请求都包括Host:头。这是唯一正确的标题。
请求URI分为两个部分 - 问号?左侧的所有内容是路径,右侧的所有内容是查询字符串请求方法 RFC 2616(HTTP / 1.1)定义了8请求方法OPTIONS OPTIONS方法很少使用。它旨在作为一种机制,在尝试消耗服务器可能提供的服务之前,确定服务器支持哪种功能。
Off the top of my head, 就我所知,在微软办公软件通过Internet Explorer直接打开文档时会使用这种方法——Office会向服务器发送一个OPTIONS请求,以判断它是否支持特定URI的PUT方法;如果支持,则以一种允许用户将更改直接保存回远程服务器的方式打开文档。这种功能密切集成在这些特定的Microsoft应用程序中。
GET
这是日常使用中最常见的方法。每次在Web浏览器中加载常规文档时,都会发出GET请求。
GET方法要求服务器返回特定文档。应该传输到服务器的唯一数据是服务器需要确定应返回哪个文档的信息。这可以包括服务器可以用来动态生成文档的信息,这些信息以请求URI中的标头和/或查询字符串形式发送。顺便说一下- Cookies以请求标头的形式发送。
HEAD

这种方法与GET方法相同,唯一的区别在于服务器不会返回请求的文档,而只会返回将包含在响应中的标头。这对于确定特定文档是否存在而无需传输和处理整个文档非常有用。

POST

这是第二种最常用的方法,也可以说是最复杂的方法。几乎所有使用POST方法的请求都是用来在服务器上调用某些动作以改变其状态。

与GET和HEAD不同,POST请求可以(并且通常会)在请求消息的正文中包含一些数据。这些数据可以是任何格式,但最常见的是查询字符串(与请求URI中出现的格式相同)或多部分消息,可以与文件附件一起传递键/值对。

许多HTML表单使用POST方法。为了从浏览器上传文件,您需要在表单中使用POST方法。

POST方法在语义上与RESTful API不兼容,因为它不是幂等的idempotent。也就是说,第二个相同的POST请求可能会导致服务器状态的进一步更改。这与REST的“无状态”约束相矛盾。

PUT

PUT方法是GET方法的补充。当使用GET请求时,服务器会在响应正文中返回请求URI指定位置的文档,而使用PUT方法时,服务器会将请求正文中的数据存储到请求URI指定的位置。

DELETE

DELETE方法表示服务器应该销毁请求URI指定位置的文档。由于非常明显,因此很少有面向Internet的HTTP服务器实现在收到DELETE请求时执行任何操作。

TRACE

TRACE提供了一种应用层级别的机制,允许客户端检查它发送的请求在到达目标服务器之前的状态。这对于确定客户端和目标服务器之间的任何代理服务器可能对请求消息产生的影响非常有用。

CONNECT

HTTP 1.1保留CONNECT方法的名称,但并未定义其用途或目的。一些代理服务器实现随后使用CONNECT方法来促进HTTP隧道。


@Maurice PUT请求会将任何表单数据作为查询字符串包含在请求URI中(与GET请求中的位置相同)。 - DaveRandom
1
@Maurice 好的,我会根据用户的ID来实现这个功能。所以他们会使用 PUT /<user-id>/contact-info.txt,然后你的应用程序可以从URI路径中提取用户ID,并使用它来确定在哪里存储文件数据 - 在本地文件系统、数据库或其他地方。我注意到最初你发送了一张图片,假设那是个人资料照片,那么用户会使用 PUT /<user-id>/profile.png - 再次使用该ID来确定如何处理数据。 - DaveRandom
1
需要记住的一件事是PUT不是特别用户友好,因为浏览器通常不容易支持它(我不知道HTML5是否为此提供了某些规定?可能)。您为此提供了什么样的接口?是基于HTML的GUI类型界面还是只需要一些编程知识的API? - DaveRandom
1
@Maurice 不用担心。我已经扩展了我的答案,供参考 - 我以前遇到过许多人对PUT方法有类似的误解,希望现在我可以直接链接他们到这里。如果你有任何不理解的地方,请告诉我,因为我想把这作为一个参考点,用(相对)简单的术语解释这一切应该如何工作(理论上)。 - DaveRandom
我的天啊.. 这是一个很好的解释。我怎么能给你点赞一千次以上呢? - Maurice
显示剩余14条评论

6

我从未尝试使用PUT(GET POST和FILES对我的需求已足够),但这个例子来自php文档,可能会对你有所帮助(http://php.net/manual/en/features.file-upload.put-method.php):

<?php
/* PUT data comes in on the stdin stream */
$putdata = fopen("php://input", "r");

/* Open a file for writing */
$fp = fopen("myputfile.ext", "w");

/* Read the data 1 KB at a time
   and write to the file */
while ($data = fread($putdata, 1024))
  fwrite($fp, $data);

/* Close the streams */
fclose($fp);
fclose($putdata);
?>

1
这听起来很合理,但是它也会输出文件头。此外,它还会将PUTDATA写入文件中。所以就像我预料的那样,这种方法会保存一个损坏的文件。 - Maurice
@Maurice:你真的尝试过了吗?还是你只是在做那个假设? - FtDRbwLXw6
1
我已经为您尝试了一下。和代码中所说的一样,它只是将php://input流中的所有内容写入文件,但在我的情况下,由于它包含除文件以外的其他数据,因此该文件会损坏。明确一下,在我的情况下,php://input流包含两个内容:1)常规PUT DATA(如表单数据)和2)上传的文件。我会更新上面的问题。感谢您的思考! - Maurice
抱歉如果我显得有些不耐烦。我真的很感激你的思考。这个文档中的例子过去曾经让我感到沮丧,因为我认为它们过于简化了。 - Maurice
原来你是对的,我理解错了我的通信方式。我期望能够在同一个请求中发送多个文件。 - Maurice
无法工作。[Fri May 09 16:14:45 2014] [error] [client 127.0.0.1] PHP解析错误:/Library/WebServer/Documents/put.php的第3行解析错误。 - Jesse Barnum

4
这里是我发现最有用的解决方案。 $put = array(); parse_str(file_get_contents('php://input'), $put); $put将会是一个数组,就像你习惯在$_POST中看到的一样,只不过现在你可以遵循真正的REST HTTP协议。

1
迄今为止这里最简单的答案。正是我在寻找的。 - Richard

1
使用POST方法并包含一个X-头来表示实际的方法(在这种情况下是PUT)。通常这是解决防火墙不允许除GET和POST方法之外的方法的方法。只需声明PHP有缺陷(因为它拒绝处理多部分PUT负载,所以它是有缺陷的),并将其视为过时/严厉的防火墙。
关于PUT与GET的关系,意见仅仅是意见。HTTP没有这样的要求。它只是简单地说明“等价”...由设计者确定什么是“等价”。如果您的设计可以接受多文件上传的PUT,并为同一资源的后续GET生成“等效”表示,那就既符合技术上也符合HTTP规范,非常好。

0

只需按照文档中所说的做即可:

<?php
/* PUT data comes in on the stdin stream */
$putdata = fopen("php://input", "r");

/* Open a file for writing */
$fp = fopen("myputfile.ext", "w");

/* Read the data 1 KB at a time
   and write to the file */
while ($data = fread($putdata, 1024))
  fwrite($fp, $data);

/* Close the streams */
fclose($fp);
fclose($putdata);
?>

这个应该读取PUT流上的整个文件并将其保存在本地,然后您可以按照自己的意愿处理它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接