Sunday 5 August 2007

Just another Phing task

Yesterday I was looking for a similarity to Ant's get task in the Phing build tool and as there was none available it was time to create on. The get task enables the using target to get a file from an Url and comes in handy when you have to add remote files to your build. The upcoming solution doesn't cover all available attributes of it's Ant relative task i.e. no Http authentication and progress output.

This table lists the available attributes of the get task and their possible default values.

NameTypeDescriptionDefaultRequired
srcstringThe source to 'get' fromn/aYes
deststringThe destination for the source filen/aYes
usetimestampstringEnables a conditional download of the source file when newer than the already 'getted' destination file. Available options are true|false.falseNo

The above table leads to the following Phing target layout where the custom get task is pulled in via the <taskdef> element.
<?xml version="1.0" ?>
<project name="example" basedir="." default="retrieve-library">

<!-- Pull in the custom get task -->
<taskdef name="get" classname="phing.tasks.my.GetTask" />

<property name="application.name" value="blogOrganizer"/>

<target name="retrieve-library">
<get src="http://framework.zend.com/images/PoweredBy_ZF_4LightBG.png"
dest="${project.basedir}/${application.name}/library/ZendFramework/PoweredBy_ZF_4LightBG.png"
usetimestamp="true"/>
</target>
</project>
The following code listing shows the internals of the GetTask class and goes to the directory /classes/phing/tasks/my and is also available for download. It utilizes the Php curl extension and provides a 'no curl extension enabled' fallback using only Php's built-in functions. Another uncovered approach for implementing this task would be to utilitize the wget command.
<?php

require_once('phing/Task.php');

class GetTask extends Task {

protected $src = null;
protected $dest = null;
protected $applyTimestampConditional = null;

protected $fpDest = null;

/**
* Initialize the GetTask.
*/
public function init() {
$this->applyTimestampConditional = false;
}
/**
* Set the source.
* @param string $src The source to get.
*/
public function setSrc($src) {
$this->src = $src;
}
/**
* Set the dest to copy/write the src to.
* @param string $src The dest for get src.
*/
public function setDest($dest) {
$this->dest = $dest;
}
/**
* Set conditional for a timestamp comparison.
* @param boolean $usetimestamp The conditional for the timestamp comparison.
*/
public function setUsetimestamp($usetimestamp) {
$this->applyTimestampConditional = $usetimestamp;
}
/**
* Get the src file and copy/write it to given dest.
* @throws BuildException
*/
public function main() {

$this->validate();

if(!file_exists($this->dest) || $this->applyTimestampConditional === false) {

try {

$kiloBytes = $this->getSrcFile();
print(" [get] Got file {$this->src} kB[{$kiloBytes}]\n");

} catch(Exception $e) {

throw new BuildException($e);

}

} else {

try {

if($this->applyTimestampConditional &&
filemtime($this->dest) === $this->getLastModificationForSrcFile($this->src)) {

print(" [get] The timestamps of src and existing dest are unique\n");

} else {

$kiloBytes = $this->getSrcFile();
print(" [get] Got file {$this->src} kB[{$kiloBytes}]\n");

}

} catch(Exception $e) {

throw new BuildException($e);

}
}

}
/**
* Validate that the required attributes have been set.
* @throws BuildException
*/
private function validate() {

if(!isset($this->src) && !isset($this->dest)) {

throw new BuildException("Required attributes 'src' and 'dest' are not set");

}

if(!isset($this->src)) {

throw new BuildException("Required attribute 'src' is not set");

}

if(!isset($this->dest)) {

throw new BuildException("Required attribute 'dest' is not set");

}
if(isset($this->applyTimestampConditional)) {

if($this->applyTimestampConditional !== false && $this->applyTimestampConditional !== true) {
throw new BuildException("Unsupported value for attribute usetimestamp set");
}
}

}
/**
* Get the src file and creates the dest file.
* Utilizes the curl extension first and uses php's default
* built-in funtions as a fallback.
* @return float The kilo bytes received from src.
* @throws Exception
*/
private function getSrcFile() {

if (function_exists('curl_init')) {

try {

$bytesCopied = $this->getSrcFileViaCurl();

} catch(Exception $e) {

throw $e;

}

} else {

try {

$bytesCopied = $this->getSrcFileViaPhpBuiltins();

} catch(Exception $e) {

throw $e;

}

}

try {

$this->touchDest();

} catch(Exception $e) {

//ignore, no rethrow as BuildException

}
return round($bytesCopied/1024, 2);

}
/**
* Get the src file and creates the dest file.
* Uses php's default built-in funtions.
* @return float The bytes received from src.
* @throws Exception
*/
private function getSrcFileViaPhpBuiltins() {

if(ini_get('allow_url_fopen') === '') {

throw new Exception("allow_url_fopen is disabled in php.ini");

}

if($src = @fopen($this->src, 'r')) {

try {

$this->createDestFile();

} catch(Exception $e) {

throw $e;
}

if($bytesCopied = stream_copy_to_stream($src, $this->fpDest)) {

fclose($src);
fclose($this->fpDest);
return $bytesCopied;

} else {

fclose($src);
fclose($this->fpDest);
throw new Exception("Unable to copy data from {$this->src} to {$this->dest}");

}

}
}
/**
* Get the src file and creates the dest file.
* Uses the curl extension.
* @return float The bytes received from src.
* @throws Exception
*/
private function getSrcFileViaCurl() {

if($ch = curl_init($this->src)) {

try {

$this->createDestFile();

} catch(Exception $e) {

throw $e;

}
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
curl_setopt($ch, CURLOPT_FILE, $this->fpDest);
curl_exec($ch);

if($httpCode = (curl_getinfo($ch, CURLINFO_HTTP_CODE)) !== 200) {

curl_close($ch);
fclose($this->fpDest);
unlink($this->dest);
throw new Exception("Got Http Code {$httpCode} for {$this->src}");

}

$bytesCopied = curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD);
curl_close($ch);
fclose($this->fpDest);
return $bytesCopied;

} else {

throw new Exception("Unable to init curl with {$this->src}");

}

}
/**
* Touch the dest file.
* @throws Exception
* @see function touch()
*/
private function touchDest() {

try {

$lastModifikationOfSrcFile = $this->getLastModificationForSrcFile();
touch($this->dest, $lastModifikationOfSrcFile);

} catch(Exception $e) {

throw $e;

}
}
/**
* Get the last modification date from src file.
* @return string The timestamp of the src file.
* @throws Exception
* @note Remixed from a php.net filemtime user note.
*/
private function getLastModificationForSrcFile($uri = null) {

$timestamp = 0;

if($fp = @fopen($this->src, "r" )) {

$metaData = stream_get_meta_data($fp);
fclose($fp);

} else {

throw new Exception("Unable to open src {$this->src}");

}

if(!array_key_exists('wrapper_data', $metaData)) {

throw new Exception("Unable to read meta data section for src {$this->src}");

}

foreach($metaData['wrapper_data'] as $response) {

if(substr(strtolower($response), 0, 10) == 'location: ') {
$newUri = substr($response, 10);
return $this->getLastModificationForRemoteFile($newUri);

} elseif(substr(strtolower($response), 0, 15 ) == 'last-modified: ') {

$timestamp = strtotime(substr($response, 15));
return $timestamp;

}
}
}
/**
* Create the dest file.
* @throws Exception
*/
private function createDestFile() {

if($this->fpDest = fopen($this->dest, 'w')) {

return true;

} else {

throw new Exception("Unable to create {$this->dest}");

}

}

}

2 comments:

fqqdk said...

raphael, why are you cluttering your code with this kind of thing all the time:

try {
doSomething();
} catch(Exception $e) {
throw $e;
}

this is the same as just writing
doSomething();
on its own.

Raphael Stolt said...

Valid point you've made. Normally I use it to add some additional messages to the Exception message or to inject logging, but since I'm not doing it here it really just clutters the code. Thanks for pointing it out to me.