<?
abstract class CDN {
    protected 
$localrootRE;
    protected 
$cdnroot;
    protected 
$currentdir=array(); // used by opendir/closedir/rewinddir

    
function __construct($cdnroot){
        
$this->cdnroot $this->slash($cdnroot);
        
$this->localrootRE =  '#^'.preg_quote ($this->slash($_SERVER['DOCUMENT_ROOT']), '#').'#';
    }

    function 
slash ($name){
        
// make sure $name ends with a slash
        
return rtrim($name'/').'/';
    }

    function 
cdn($filename){ // replace the prefix with the CDN address
        // simple correction for special characters
        
$filename str_replace (' ''+'$filename);
        return 
preg_replace($this->localrootRE$this->cdnroot$filename);
    }
    
    function 
isCDN($filename){
        return 
strpos($filename$this->cdnroot) !== FALSE;
    }
    
    function 
realpath($path){
        
$realpath realpath($path);
        if (
$realpath !== FALSE) return $realpath// assumes the 5.2 realpath bug has been corrected
        
if ($path[0] != DIRECTORY_SEPARATOR$path getcwd().DIRECTORY_SEPARATOR.$path// quickie check for relative paths
        // does not handle .. and . or multiple //
        
return $this->cdn($path);
    }

    abstract function 
filemtime($filename);

    abstract function 
file_exists($filename);

    abstract function 
scandir ($dirname);

    function 
opendir($dirname){
        
$this->currentdir $this->scandir($dirname);
        return 
$this->currentdir;
    }

    function 
closedir($dir=NULL){ // only works on the current directory
        
$this->currentdir = array();
    }

    function 
rewinddir(&$dir=NULL){
        
reset($this->currentdir);
    }

    function 
readdir(&$dir=NULL){
        
$result each($this->currentdir);
        if (
$result==FALSE) return FALSE;
        return 
$result['value'];
    }
}

class 
S3 extends CDN {
    
// functions specific to amazon s3
    
function filemtime($filename) {
        if (
realpath($filename) !== FALSE) return filemtime($filename);
        
$filename $this->cdn($filename);
        
$ch curl_init($filename);
        
curl_setopt($chCURLOPT_RETURNTRANSFERTRUE);
        
curl_setopt($chCURLOPT_HEADERTRUE);
        
curl_setopt($chCURLOPT_NOBODYTRUE);
        
$info curl_exec($ch);
        
curl_close($ch);
        
preg_match('/Last-Modified:(.*)\n/'$info$matches);
        return 
strtotime(isset($matches[1]) ? $matches[1] : 'today');
    }

    function 
file_exists($filename) {
        if (
realpath($filename) !== FALSE) return true;
        
$filename $this->cdn($filename);
        
$ch curl_init($filename);
        
curl_setopt($chCURLOPT_NOBODYTRUE);
        
curl_exec($ch);
        
$ret curl_getinfo($chCURLINFO_HTTP_CODE) < 400;
        
curl_close($ch);
        return 
$ret;
    }
    
    function 
scandir ($dirname){
        
$ret = @scandir($dirname); // union of local and CDN files
        
if ($ret === FALSE$ret = array('..');
        
$dirname $this->slash($this->bare($dirname));
        
$encodeddirname urlencode($dirname);
        
$lastFound ''// for truncated lists, keep track of the last item returned
        
do{
            
$xml = new SimpleXMLElement(file_get_contents("$this->cdnroot?delimiter=/&marker=$lastFound&prefix=$encodeddirname"));
            foreach (
$xml->Contents as $content){ // "files", though key == dirname for the directory itself
                    
$ret[] = $content->Key == $dirname '.' basename($content->Key);    
            }
            foreach (
$xml->CommonPrefixes as $content){ // "directories"
                    
$ret[] = basename($content->Prefix);;    
            }
            
$lastFound urlencode($xml->NextMarker);
        }while (
$xml->IsTruncated == 'true');
        return 
array_unique($ret);
    }
    
    function 
bare($filename){ // remove the prefix
        
$filename preg_replace($this->localrootRE''$filename);
        
$s3rootRE =  '#^'.preg_quote ($this->cdnroot'#').'#';
        
$filename preg_replace($s3rootRE''$filename);
        return 
$filename;
    }

}
?>