I'm attempting to write a file to a server from my local machine using LibCURL, C++, and php, and I am running into an error, instead of the form completing, I am getting the error message "411 length required".
I am working from this example
http://curl.haxx.se/libcurl/c/postit2.html
This is my php form which I have verified as working
<!DOCTYPE HTML>
<html>
<head>
</head>
<body>
<form method="post" enctype="multipart/form-data" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>">
Enter file: <input type="file" name="sendfile" size="40">
<input type="submit" value="send" name="submit">
</form>
<?php
echo “request method: " . $_SERVER[REQUEST_METHOD];
if( "$_SERVER[REQUEST_METHOD]" == "POST" || "$_SERVER[REQUEST_METHOD]" == "PUT")
{
echo "Success <br>";
if( $_FILES['sendfile']['name'] != "" )
{
$target_dir = "test/";
$target_file = $target_dir . basename($_FILES["sendfile"]["name"]);
if (move_uploaded_file($_FILES["sendfile"]["tmp_name"], $target_file))
{
echo "The file ". basename( $_FILES["sendfile"]["name"]). " has been uploaded.";
}
else
{
echo "Sorry, there was an error uploading your file.";
}
}
else
{
die("No file specified!");
}
}
?>
</body>
</html>
And this is the libcurl code which is pretty faithful to the example apart from the 3 lines in the set upload file section. This is something which I tried based on a different example.
//setup
curl_global_init(CURL_GLOBAL_ALL);
handle = curl_easy_init();
post = NULL;
last = NULL;
header_list = NULL;
uploadFile = NULL;
curl_easy_setopt(handle, CURLOPT_NOPROGRESS, ofGetLogLevel() <= OF_LOG_VERBOSE ? 0 : 1);
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, true);
curl_easy_setopt(handle, CURLOPT_VERBOSE, ofGetLogLevel() <= OF_LOG_VERBOSE);
curl_easy_setopt(handle, CURLOPT_CAINFO, ofToDataPath("cacert.pem").c_str());
curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, content_writer);
curl_easy_setopt(handle, CURLOPT_WRITEDATA, &content);
curl_easy_setopt(handle, CURLOPT_HEADERFUNCTION, header_writer);
curl_easy_setopt(handle, CURLOPT_WRITEHEADER, &header);
//seturl
curl_easy_setopt(handle, CURLOPT_URL, “link to hosted php file on server”);
//add form field
curl_formadd(&post, &last, CURLFORM_COPYNAME, "sendfile", CURLFORM_FILE, “filepath on my local system”, CURLFORM_END);
//set upload file
FILE* uploadFile = fopen(“filepath on my local system”, "rb");
curl_easy_setopt(handle, CURLOPT_UPLOAD, 1);
curl_easy_setopt(handle, CURLOPT_READDATA, uploadFile);
//perform
CURLcode ret = curl_easy_setopt(handle, CURLOPT_VERBOSE, ofGetLogLevel() <= OF_LOG_VERBOSE);
ret = curl_easy_setopt(handle, CURLOPT_NOPROGRESS, ofGetLogLevel() <= OF_LOG_VERBOSE ? 0 : 1);
ret = curl_easy_setopt(handle, CURLOPT_HTTPPOST, post);
ret = curl_easy_perform(handle);
curl_formfree(post);
post = NULL;
fclose(uploadFile);
uploadFile = NULL;
I have tried adding a header with the string "Content-Length: 0" and "Content-Length: 44000" (the size of the test jpg) but neither change the error.
there are similar questions on here but only one is using c++ which is this one
error 411 Length Required c++, libcurl PUT request
but it doesn't quite fit my situation and doesn't provide any answers either.
Set CURLOPT_INFILESIZE like so:
curl_easy_setopt(handle, CURLOPT_INFILESIZE, 44000);
Related
I have spent a couple of hours reading up on this but as so yet I find no clear solutions....I am using WAMP to run as my Local server. I have a successful API call set up to return data.
I would like to store that data locally, thus reducing the number of API call being made.
For simplicity I have created a cache.json file in the same folder as my PHP scripts and when I run the process I can see the file has been accessed as the time stamp updates.
But the file remains empty.
Based on research I suspect the issue may come down to a permission issue; I have gone through the folders and files and unchecked read only etc.
Appreciate if someone could validate that my code is correct and if it is hopefully point me in the the direction of a solution.
many thanks
<?php
$url = 'https://restcountries.eu/rest/v2/name/'. $_REQUEST['country'];
$cache = __DIR__."/cache.json"; // make this file in same dir
$force_refresh = true; // dev
$refresh = 60; // once an min (set short time frame for testing)
// cache json results so to not over-query (api restrictions)
if ($force_refresh || ((time() - filectime($cache)) > ($refresh) || 0 == filesize($cache))) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL,$url);
$result=curl_exec($ch);
curl_close($ch);
$decode = json_decode($result,true);
$handle = fopen($cache, 'w');// or die('no fopen');
$json_cache = $decode;
fwrite($handle, $json_cache);
fclose($handle);
}
} else {
$json_cache = file_get_contents($cache); //locally
}
echo json_encode($json_cache, JSON_UNESCAPED_UNICODE);
?>
I managed to solve this by using file_put_contents(), not being an expert I do not understand why this works and the code above doesn't, but maybe this helps someone else.
adjusted code:
<?php
$url = 'https://restcountries.eu/rest/v2/name/'. $_REQUEST['country'];
$cache = __DIR__."/cache.json"; // make this file in same dir
$force_refresh = false; // dev
$refresh = 60; // once an min (short time frame for testing)
// cache json results so to not over-query (api restrictions)
if ($force_refresh || ((time() - filectime($cache)) > ($refresh) || 0 == filesize($cache))) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL,$url);
$result=curl_exec($ch);
curl_close($ch);
$decode = json_decode($result,true);
$handle = fopen($cache, 'w');// or die('no fopen');
$json_cache = $result;
file_put_contents($cache, $json_cache);
} else {
$json_cache = file_get_contents($cache); //locally
$decode = json_decode($json_cache,true);
}
echo json_encode($decode, JSON_UNESCAPED_UNICODE);
?>
I am trying to get song name / artist name / song length / bitrate etc from a remote .mp3 file such as http://shiro-desu.com/scr/11.mp3 .
I have tried getID3 script but from what i understand it doesn't work for remote files as i got this error: "Remote files are not supported - please copy the file locally first"
Also, this code:
<?php
$tag = id3_get_tag( "http://shiro-desu.com/scr/11.mp3" );
print_r($tag);
?>
did not work either.
"Fatal error: Call to undefined function id3_get_tag() in /home4/shiro/public_html/scr/index.php on line 2"
As you haven't mentioned your error I am considering a common error case undefined function
The error you get (undefined function) means the ID3 extension is not enabled in your PHP configuration:
If you dont have Id3 extension file .Just check here for installation info.
Firstly, I didn’t create this, I’ve just making it easy to understand with a full example.
You can read more of it here, but only because of archive.org.
https://web.archive.org/web/20160106095540/http://designaeon.com/2012/07/read-mp3-tags-without-downloading-it/
To begin, download this library from here: http://getid3.sourceforge.net/
When you open the zip folder, you’ll see ‘getid3’. Save that folder in to your working folder.
Next, create a folder called “temp” in that working folder that the following script is going to be running from.
Basically, what it does is download the first 64k of the file, and then read the metadata from the file.
I enjoy a simple example. I hope this helps.
<?php
require_once("getid3/getid3.php");
$url_media = "http://example.com/myfile.mp3"
$a=getfileinfo($url_media);
echo"<pre>";
echo $a['tags']['id3v2']['album'][0] . "\n";
echo $a['tags']['id3v2']['artist'][0] . "\n";
echo $a['tags']['id3v2']['title'][0] . "\n";
echo $a['tags']['id3v2']['year'][0] . "\n";
echo $a['tags']['id3v2']['year'][0] . "\n";
echo "\n-----------------\n";
//print_r($a['tags']['id3v2']['album']);
echo "-----------------\n";
//print_r($a);
echo"</pre>";
function getfileinfo($remoteFile)
{
$url=$remoteFile;
$uuid=uniqid("designaeon_", true);
$file="temp/".$uuid.".mp3";
$size=0;
$ch = curl_init($remoteFile);
//==============================Get Size==========================//
$contentLength = 'unknown';
$ch1 = curl_init($remoteFile);
curl_setopt($ch1, CURLOPT_NOBODY, true);
curl_setopt($ch1, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch1, CURLOPT_HEADER, true);
curl_setopt($ch1, CURLOPT_FOLLOWLOCATION, true); //not necessary unless the file redirects (like the PHP example we're using here)
$data = curl_exec($ch1);
curl_close($ch1);
if (preg_match('/Content-Length: (\d+)/', $data, $matches)) {
$contentLength = (int)$matches[1];
$size=$contentLength;
}
//==============================Get Size==========================//
if (!$fp = fopen($file, "wb")) {
echo 'Error opening temp file for binary writing';
return false;
} else if (!$urlp = fopen($url, "r")) {
echo 'Error opening URL for reading';
return false;
}
try {
$to_get = 65536; // 64 KB
$chunk_size = 4096; // Haven't bothered to tune this, maybe other values would work better??
$got = 0; $data = null;
// Grab the first 64 KB of the file
while(!feof($urlp) && $got < $to_get) { $data = $data . fgets($urlp, $chunk_size); $got += $chunk_size; } fwrite($fp, $data); // Grab the last 64 KB of the file, if we know how big it is.
if ($size > 0) {
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RESUME_FROM, $size - $to_get);
curl_exec($ch);
}
// Now $fp should be the first and last 64KB of the file!!
#fclose($fp);
#fclose($urlp);
}
catch (Exception $e) {
#fclose($fp);
#fclose($urlp);
echo 'Error transfering file using fopen and cURL !!';
return false;
}
$getID3 = new getID3;
$filename=$file;
$ThisFileInfo = $getID3->analyze($filename);
getid3_lib::CopyTagsToComments($ThisFileInfo);
unlink($file);
return $ThisFileInfo;
}
?>
I have an HTML code that allows me to upload the file successfully in server.
<form action="http://aa.bb.ccc.dd/xxx/upload.php" method="post" enctype="multipart/form-data">
<label for="text">Campaign:</label>
<input type="text" name="campaign" value="abcde" readonly="readonly"/><br/>
<label for="file">Upload type:</label>
<input type="text" name="filename" value="0.csv" readonly="readonly"/><br/>
<label for="file">Filename:</label>
<input type="file" name="file" id="file"/><br/>
<input type="submit" name="submit" value="Submit" />
</form>
I am writing an equivalent code in php to upload a file via curl. But the file does not get uploaded. Can anyone please help me on this. My php server code is as follows:
<code>$target_url = 'http://aa.bb.ccc.dd/xxx/upload.php';
$file_name_with_full_path = realpath('./upload/abc.txt');
$post = array('campaign' => 'abcde','file'=>'#'.$file_name_with_full_path,'filename'=>'5.csv');
$header = array('Content-Type: multipart/form-data');
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$target_url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_POST,1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
$result=curl_exec ($ch);
curl_close ($ch);
echo "Result: ".$result."\n";</code>
The result also gives 1, but the file is not uploaded when I check in the server. What is that I am missing?
Any help would be greatly appreciated!
I am interested in using PHP 'cURL'. This looks like a fairly standard requirement.
I looked at some of the 'cURL PHP examples on the web'.
Actually, using the code you have posted, there isn't anything really amiss that i can see.
Whatever, i have used your code and created similar scripts. Alas, you didn't post your 'upload.php' script. I have created one that does validation as mentioned in the PHP manual: http://www.php.net/manual/en/features.file-upload.php.
Although this example is for a 'localhost'. I have run it using a ' real' external host and it works fine.
Tested on PHP 5.3.18 on windows and Linux with PHP 5.3.28.
The Html form, was some confusion with labels as regards 'for':
<form action="process_uploaded_file.php" method="post" enctype="multipart/form-data">
<label for="text">Campaign:</label>
<input type="text" name="campaign" value="abcde" readonly="readonly"/><br/>
<label for="filetype">Upload type:</label>
<input type="text" name="filetype" value="0.csv" readonly="readonly"/><br/>
<label for="file">Filename:</label>
<input type="file" name="file" id="file"/><br/>
<input type="submit" name="submit" value="Submit" />
</form>
cURL Script:
<?php
$target_url = 'http://localhost/testmysql/process_uploaded_file.php';
$full_path_to_source_file = __DIR__ .'/sourcefiles/testupload1.csv' ;
$post = array('campaign' => 'abcde', 'file'=>'#'. $full_path_to_source_file, 'filename' => 'curl1.csv');
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$target_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
$result=curl_exec ($ch);
curl_close ($ch);
var_dump($result);
Notice, no special 'enctype' header was required. Used the 'RETURNTRANSFER' option explicitly although the information was returned anyway.
Process Uploaded File script:
Implements a lot of recommended checks.
<?php session_start();
define('BIGGEST_FILE', 256 * 1024); // max upload file size
define('UPLOAD_DIRECTORY', 'P:/developer/xampp/htdocs/uploadedfiles'); // my data upload directory
if (empty($_FILES)) { // process the uploaded file...
die('no input file provided... '. __FILE__.__LINE__);
}
/* */
// validate the data -- see http://www.php.net/manual/en/features.file-upload.php
try {
// Undefined | Multiple Files | $_FILES Corruption Attack
// If this request falls under any of them, treat it invalid.
if ( !isset($_FILES['file']['error'])
|| is_array($_FILES['file']['error'])) {
throw new RuntimeException('Invalid parameters.');
}
// Check $_FILES['file']['error'] value.
switch ($_FILES['file']['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
throw new RuntimeException('No file sent.');
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
throw new RuntimeException('Exceeded filesize limit.');
default:
throw new RuntimeException('Unknown errors.');
}
// You should also check filesize here.
if ($_FILES['file']['size'] > BIGGEST_FILE) {
throw new RuntimeException('Exceeded filesize limit.');
}
// DO NOT TRUST $_FILES['file']['mime'] VALUE !!
// Check MIME Type by yourself.
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $_FILES['file']['tmp_name']);
/* */
if (false === $fileExt = array_search($mimeType,
array(
'jpg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'csv' => 'text/plain',
),
true
)) {
throw new RuntimeException('Invalid file format.');
}
// check 'campaign' for safe in filename...
if (preg_match('/\w/', $_POST['campaign']) !== false) {
$campaign = $_POST['campaign'];
}
else {
$campaign = md5($_POST['campaign']); // sort of useful
}
// Now move the file to my data directory
// You should name it uniquely.
// DO NOT USE $_FILES['file']['name'] WITHOUT ANY VALIDATION !!
// On this example, obtain safe unique name from its 'campaign' and 'tmp_name'.
$destFilename = sprintf('campaign_%s_%s.%s',
$campaign,
sha1_file($_FILES['file']['tmp_name']),
$fileExt);
if (!move_uploaded_file($_FILES['file']['tmp_name'],
UPLOAD_DIRECTORY .'/'. $destFilename)) {
throw new RuntimeException('Failed to move uploaded file.');
}
echo $_FILES['file']['tmp_name'], ' uploaded to: ', UPLOAD_DIRECTORY .'/'. $destFilename;
} catch (RuntimeException $e) {
echo $e->getMessage();
}
I am developing a web application that uses JQuery AJAX and PHP to upload some data into my database.
One of the fields of the form to be submitted is an URL of a image (any address of the WEB). This image should be downloaded to my FTP server and then its new addrress would be inserted into the database.
How can I download an image from any URL and upload it to my FTP server?
Form:
<form id="form-id" method="post" action="insert.php" charset=utf-8">
<input type="text" name="title" id="title">
<input type="text" name="image-url" id="image-url">
<input type="submit" name="submit" id="submit">
</form>
JavaScript
$("#submit").live("click", function(event){
event.preventDefault();
$.ajax({
type : "POST",
url : "insert.php",
data : {
'title': valueTitle,
'image': valueImage
},
cache : false,
success : function(html) {
if (html == "success") {
//...
} else if (html == "ftp-error") {
//...
} else if (html == "sql-error") {
//...
}
}
});
});
insert.php
$title = $_REQUEST['title'];
$image = $_REQUEST['image'];
$imageInMyServer = downloadImageFromURLAndUploadFTP($image);
function downloadImageFromURLAndUploadFTP($image) {
//that is what I want to know how to do.
}
//sql query with $title and $imageInMyServer
Notes:
The file I want to download is not on my server. It is somewhere else in the Internet and I need to download it to my FTP server
No. I cannot use the first external URL in my SQL Query
Here is a great example on how to do FTP transfers in PHP. As far as downloading the file, you could use wget if you're on linux (using the exec() function).
exec('wget -q ' . $url . ' -0 /path/to/newfile');
Stealing a code snippet from that link I gave you, here is what your function might look like:
function downloadImageFromURLAndUploadFTP($image) {
// in your case it would be some img extension like .jpg, .gif, or .png
// you can check the extension of $image and use that if you want.
$newFile = '/path/to/newfile.ext';
exec('wget -q ' . $image . ' -0 ' . $newFile);
if (file_exists($newFile)) {
// set up connection and login
$connect = ftp_connect($ftpServer);
$login = ftp_login($connect, $ftpUser, $ftpPass);
// check connection
if (!$connect || !$login) {
die('FTP connection has failed!');
} else {
echo "Connected to {$ftpServer}, for user {$ftpUser}";
}
// upload the file
$fileNameOnFTPServer = 'whateverYouWantToNameIt.ext'; // arbitrary extension
$upload = ftp_put($connect, $fileNameOnFTPServer, $newFile, FTP_BINARY);
// check upload status
if (!$upload) {
echo "FTP upload has failed!";
} else {
echo "Uploaded {$image} to {$ftpServer} as {$fileNameOnFTPServer}";
}
ftp_close($connect);
}
}
Note: Sometimes file_exists() doesn't behave the way we intended when the path begins with /. For example /path/to/file might exist but file_exists() will think it doesn't unless you remove the beginning "/". One way to get around that is to check it like this:
file_exists(substr($newFile, 1))
Good luck!
An alternative solution if you do not have exec privileges is to use curl to grab the image, or you could use file_get_contents(), there are many ways, its just personal preference.
Ive put together what your script may look like, im sure you can improve it.
insert.php
<?php
if(isset($_POST['image']) && isset($_POST['title'])){
if(substr($_POST['image'],0,4)=='http'){
$image = curlgetimage($_POST['image']);
$info = pathinfo($_POST['image']);
if(isset($info['extension']) && ($info['extension']=='gif' || $info['extension']=='png' || $info['extension']=='jpg')){
$path='./temp/'.md5($_POST['image']).'.'.$info['extension'];
file_put_contents($path,$image);
if(ftp_put_image($path)===true){
//Do your database stuff, remember to escape..
unlink($path);
echo 'Success';
}else{
echo 'ftp-fail';
}
}else{
echo'File type not allowed';
}
}else{
echo'Must start with http://';
}
}else{
header('Location: http://www.example.com/');
}
function ftp_put_image($file){
if(!file_exists($file)){return false;}
$fp = fopen($file, 'r');
$conn_id = ftp_connect('ftp.yourhost.com'); //change
$login_result = ftp_login($conn_id,'username','password'); //change
$return=(ftp_fput($conn_id, $file, $fp, FTP_BINARY))?true:false;
ftp_close($conn_id);
fclose($fp);
return $return;
}
function curlgetimage($url) {
$header[] = 'Accept: image/gif, image/x-bitmap, image/jpeg, image/pjpeg';
$header[] = 'Connection: Keep-Alive';
$header[] = 'Content-type: application/x-www-form-urlencoded;charset=UTF-8';
$curl = curl_init($url);
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_USERAGENT, 'YourSpiderBot/0.01 (Bla Bla Robot; http://www.example.com; spider#example.com)'); //change
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
curl_setopt($curl, CURLOPT_HEADER, 0);
curl_setopt($curl, CURLOPT_REFERER, $url);
curl_setopt($curl, CURLOPT_ENCODING, 'gzip,deflate');
curl_setopt($curl, CURLOPT_AUTOREFERER, true);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_TIMEOUT, 60);
$return = curl_exec($curl);
curl_close($curl);
return $return;
}
?>
I'm using cURL to transfer image files from one server to another using PHP. This is my cURL code:
// Transfer the original image and thumbnail to our storage server
$ch = curl_init();
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_VERBOSE, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, 'http://' . $server_data['hostname'] . '.localhost/transfer.php');
curl_setopt($ch, CURLOPT_POST, true);
$post = array(
'upload[]' => '#' . $tmp_uploads . $filename,
'upload[]' => '#' . $tmp_uploads . $thumbname,
'salt' => 'q8;EmT(Vx*Aa`fkHX:up^WD^^b#<Lm:Q'
);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
$resp = curl_exec($ch);
This is the code in transfer.php on the server I'm uploading to:
if($_FILES && $_POST['salt'] == 'q8;EmT(Vx*Aa`fkHX:up^WD^^b#<Lm:Q')
{
// Save the files
foreach($_FILES['upload']['error'] as $key => $error)
{
if ($error == UPLOAD_ERR_OK)
{
move_uploaded_file($_FILES['upload']['tmp_name'][$key], $_FILES['upload']['name'][$key]);
}
}
}
All seems to work, apart from one small logic error. Only one file is getting saved on the server I'm transferring to. This is probably because I'm calling both images upload[] in my post fields array, but I don't know how else to do it. I'm trying to mimic doing this:
<input type="file" name="upload[]" />
<input type="file" name="upload[]" />
Anyone know how I can get this to work? Thanks!
here is your error in the curl call...
var_dump($post)
you are clobbering the array entries of your $post array since the key strings are identical...
make this change
$post = array(
'upload[0]' => '#' . $tmp_uploads . $filename,
'upload[1]' => '#' . $tmp_uploads . $thumbname,
'salt' => 'q8;EmT(Vx*Aa`fkHX:up^WD^^b#<Lm:Q'
);
The code itself looks ok, but I don't know about your move() target directory. You're using the raw filename as provided by the client (which is your curl script). You're using the original uploaded filename (as specified in your curl script) as the target of the move, with no overwrite checking and no path data. If the two uploaded files have the same filename, you'll overwrite the first processed image with whichever one got processed second by PHP.
Try putting some debugging around the move() command:
if (!move_uploaded_file($_FILES['upload']['tmp_name'][$key], $_FILES['upload']['name'][$key])) {
echo "Unable to move $key/";
echo $_FILES['upload']['tmp_name'][$key];
echo ' to ';
echo $_FILES['upload']['name'][$key];
}
(I split the echo onto multiple lines for legibility).