POSTing Composite Data
In the life of any programmer, problems come across that catch a person. I donβt like the standard solution and that's it! And sometimes it happens that standard solutions do not work for some reason. Some people bypass such tasks, while others like to solve them. You can even say they find them themselves. One of such tasks is sending a file or several files using the POST method.
Some will probably say that this task is not a task at all. After all, there is a wonderful CURL library that is quite simple and solves this problem easily! But don't be in a hurry. Yes, CURL is a powerful library, yes, it loads files, but ... As you know, it has a little feature - the file must be placed on your hard drive!
Now let's imagine such a situation, you are dynamically generating a file, or it is already in memory and you need to send it using the POST method to a remote Web server. What happens then? Do you need to save it before sending it? This is exactly what 90% of programmers would do. Why look for unnecessary problems if the solution lies on the surface? But we are not with you from these 90%! We are better, we can solve any problem. Why do we need an extra action? First, it uses the not fast file system of the hard drive. Secondly, we may not have access to the file system, or too little space is allocated there.
How can we then solve this problem? To do this, you need to look at how the data is actually transmitted by the POST method. The only solution is to transfer the file as a compound request usingmultipart / form-data . This technique is well documented in RFC7578 . Let's take a look at how the body of a multipart / form-data POST request will look like:
POST /form.html HTTP / 1.1 Host: server.com Referer: http://server.com/form.html User-Agent: Mozilla Content-Type: multipart / form-data; boundary = ------------- 573cf973d5228 Content-Length: 288 Connection: keep-alive Keep-Alive: 300 (empty line) (missing preamble) --------------- 573cf973d5228 Content-Disposition: form-data; name = "field" text --------------- 573cf973d5228 Content-Disposition: form-data; name = "file"; filename = "sample.txt" Content-Type: text / plain Content file --------------- 573cf973d5228--
Our body consists of two parts, in the first part we pass the value of the form field name = "field" equal to: text . In the second part, we pass the field name = "file" with the content of the file filename = "sample.txt": Content file . In the header, we specify the format of the content of the POST request - Content-Type: multipart / form-data , the separator string of parts: boundary = ------------- 573cf973d5228 and the length of the message - Content-Length: 288 .
It remains, in fact, to write a program that implements this method. Since we are smart people and do not write the same thing a hundred times in different projects, we will arrange everything in the form of a class that implements this method. Plus, let's expand it for different options for sending both files and simple form elements. And to distinguish the presence of a file among the POST data array, let's create a separate file - a container with the contents of the file and its data (name and extension). Thus, it will look like this:
<pre>
class oFile
{
private $name;
private $mime;
private $content;
public function __construct($name, $mime=null, $content=null)
{
// , $content=null, $name -
if(is_null($content))
{
// (, )
$info = pathinfo($name);
//
if(!empty($info['basename']) && is_readable($name))
{
$this->name = $info['basename'];
// MIME
$this->mime = mime_content_type($name);
//
$content = file_get_contents($name);
//
if($content!==false) $this->content = $content;
else throw new Exception('Don`t get content - "'.$name.'"');
} else throw new Exception('Error param');
} else
{
//
$this->name = $name;
// MIME
if(is_null($mime)) $mime = mime_content_type($name);
// MIME
$this->mime = $mime;
//
$this->content = $content;
};
}
//
public function Name() { return $this->name; }
// MIME
public function Mime() { return $this->mime; }
//
public function Content() { return $this->content; }
};
</pre>
Now the class itself for forming the body of multipart / form-data for the POST request:
<pre>
class BodyPost
{
//
public static function PartPost($name, $val)
{
$body = 'Content-Disposition: form-data; name="' . $name . '"';
// oFile
if($val instanceof oFile)
{
//
$file = $val->Name();
// MIME
$mime = $val->Mime();
//
$cont = $val->Content();
$body .= '; filename="' . $file . '"' . "\r\n";
$body .= 'Content-Type: ' . $mime ."\r\n\r\n";
$body .= $cont."\r\n";
} else $body .= "\r\n\r\n".urlencode($val)."\r\n";
return $body;
}
// POST
public static function Get(array $post, $delimiter='-------------0123456789')
{
if(is_array($post) && !empty($post))
{
$bool = false;
//
foreach($post as $val) if($val instanceof oFile) {$bool = true; break; };
if($bool)
{
$ret = '';
// , POST
foreach($post as $name=>$val)
$ret .= '--' . $delimiter. "\r\n". self::PartPost($name, $val);
$ret .= "--" . $delimiter . "--\r\n";
} else $ret = http_build_query($post);
} else throw new \Exception('Error input param!');
return $ret;
}
};
</pre>
This class consists of several methods. The PartPost method forms the separate parts of the composite request, and the Get method combines these parts and forms the body of the POST request in the format - multipart / form-data.
We now have a generic class for sending the body of a POST request. It remains to write a program that uses this class to send files to a remote Web server. Let's use the CURL library:
// -
include "ofile.class.php";
// POST
include "bodypost.class.php";
// POST
$delimiter = '-------------'.uniqid();
// oFile
$file = new oFile('sample.txt', 'text/plain', 'Content file');
// POST
$post = BodyPost::Get(array('field'=>'text', 'file'=>$file), $delimiter);
// CURL
$ch = curl_init();
//
curl_setopt($ch, CURLOPT_URL, 'http://server/upload/');
// , POST
curl_setopt($ch, CURLOPT_POST, 1);
// POST
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
/* :
Content-Type - ,
boundary -
Content-Length - */
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: multipart/form-data; boundary=' . $delimiter,
'Content-Length: ' . strlen($post)));
// POST Web
curl_exec($ch);
If CURL is not suitable, then this library can be used for sending over sockets. Well, actually links to sources:
- php.net documentation site
- CURL article : POST request, composite content
- wikipedia: multipart / form-data
- RFC7578
In the next article I will provide you with information on how to download large files from remote Web servers in multiple streams at the specified speed. To everyone who read to the end, thank you for your attention!