Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

607 lines (529 sloc) 17.427 kb
<?php
/*
* Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
/**
* Provides an interface for accessing Amazon S3 using PHP's native file management functions.
*
* Amazon S3 file patterns take the following form: <code>s3://bucket/object</code>.
*/
class S3StreamWrapper
{
/**
* @var array An array of AmazonS3 clients registered as stream wrappers.
*/
protected static $_clients = array();
/**
* Registers the S3StreamWrapper class as a stream wrapper.
*
* @param AmazonS3 $s3 (Optional) An instance of the AmazonS3 client.
* @param string $protocol (Optional) The name of the protocol to register.
* @return boolean Whether or not the registration succeeded.
*/
public static function register(AmazonS3 $s3 = null, $protocol = 's3')
{
S3StreamWrapper::$_clients[$protocol] = $s3 ? $s3 : new AmazonS3();
return stream_wrapper_register($protocol, 'S3StreamWrapper');
}
/**
* Makes the given token PCRE-compatible.
*
* @param string $token (Required) The token
* @return string The PCRE-compatible version of the token
*/
public static function regex_token($token)
{
$token = str_replace('/', '\/', $token);
$token = quotemeta($token);
return str_replace('\\\\', '\\', $token);
}
public $position = 0;
public $path = null;
public $file_list = null;
public $open_file = null;
public $seek_position = 0;
public $eof = false;
public $buffer = null;
public $object_size = 0;
/**
* Fetches the client for the protocol being used.
*
* @param string $protocol (Optional) The protocol associated with this stream wrapper.
* @return AmazonS3 The S3 client associated with this stream wrapper.
*/
public function client($protocol = null)
{
if ($protocol == null)
{
if ($parsed = parse_url($this->path))
{
$protocol = $parsed['scheme'];
}
else
{
trigger_error(__CLASS__ . ' could not determine the protocol of the stream wrapper in use.');
}
}
return self::$_clients[$protocol];
}
/**
* Parses an S3 URL into the parts needed by the stream wrapper.
*
* @param string $path The path to parse.
* @return array An array of 3 items: protocol, bucket, and object name ready for <code>list()</code>.
*/
public function parse_path($path)
{
$url = parse_url($path);
return array(
$url['scheme'], // Protocol
$url['host'], // Bucket
(isset($url['path']) ? substr($url['path'], 1) : ''), // Object
);
}
/**
* Close directory handle. This method is called in response to <php:closedir()>.
*
* Since Amazon S3 doesn't have real directories, always return <code>true</code>.
*
* @return boolean
*/
public function dir_closedir()
{
$this->position = 0;
$this->path = null;
$this->file_list = null;
$this->open_file = null;
$this->seek_position = 0;
$this->eof = false;
$this->buffer = null;
$this->object_size = 0;
return true;
}
/**
* Open directory handle. This method is called in response to <php:opendir()>.
*
* @param string $path (Required) Specifies the URL that was passed to <php:opendir()>.
* @param integer $options (Required) Not used. Passed in by <php:opendir()>.
* @return boolean Returns <code>true</code> on success or <code>false</code> on failure.
*/
public function dir_opendir($path, $options)
{
$this->path = $path;
list($protocol, $bucket, $object_name) = $this->parse_path($path);
$pattern = '/^' . self::regex_token($object_name) . '(.*)[^\/$]/';
$this->file_list = $this->client($protocol)->get_object_list($bucket, array(
'pcre' => $pattern
));
return (count($this->file_list)) ? true : false;
}
/**
* This method is called in response to <php:readdir()>.
*
* @return string Should return a string representing the next filename, or <code>false</code> if there is no next file.
*/
public function dir_readdir()
{
if (isset($this->file_list[$this->position]))
{
$out = $this->file_list[$this->position];
$this->position++;
}
else
{
$out = false;
}
return $out;
}
/**
* This method is called in response to <php:rewinddir()>.
*
* Should reset the output generated by <php:streamWrapper::dir_readdir()>. i.e.: The next call to
* <php:streamWrapper::dir_readdir()> should return the first entry in the location returned by
* <php:streamWrapper::dir_opendir()>.
*
* @return boolean Returns <code>true</code> on success or <code>false</code> on failure.
*/
public function dir_rewinddir()
{
$this->position = 0;
return true;
}
/**
* Create a new bucket. This method is called in response to <php:mkdir()>.
*
* @param string $path (Required) The bucket name to create.
* @param integer $mode (Optional) Permissions. 700-range permissions map to ACL_PUBLIC. 600-range permissions map to ACL_AUTH_READ. All other permissions map to ACL_PRIVATE. Expects octal form.
* @param integer $options (Optional) Ignored.
* @return boolean Whether the bucket was created successfully or not.
*/
public function mkdir($path, $mode, $options)
{
// Get the value that was *actually* passed in as mode, and default to 0
$trace_slice = array_slice(debug_backtrace(), -1);
$mode = isset($trace_slice[0]['args'][1]) ? decoct($trace_slice[0]['args'][1]) : 0;
$this->path = $path;
list($protocol, $bucket, $object_name) = $this->parse_path($path);
if (in_array($mode, range(700, 799)))
{
$acl = AmazonS3::ACL_PUBLIC;
}
elseif (in_array($mode, range(600, 699)))
{
$acl = AmazonS3::ACL_AUTH_READ;
}
else
{
$acl = AmazonS3::ACL_PRIVATE;
}
$client = $this->client($protocol);
$region = $client->hostname;
$response = $client->create_bucket($bucket, $region, $acl);
return $response->isOK();
}
/**
* Renames a file or directory. This method is called in response to <php:rename()>.
*
* @param string $path_from (Required) The URL to the current file.
* @param string $path_to (Required) The URL which the <code>$path_from</code> should be renamed to.
* @return boolean Returns <code>true</code> on success or <code>false</code> on failure.
*/
public function rename($path_from, $path_to)
{
list($protocol, $from_bucket_name, $from_object_name) = $this->parse_path($path_from);
list($protocol, $to_bucket_name, $to_object_name) = $this->parse_path($path_to);
$copy_response = $this->client($protocol)->copy_object(
array('bucket' => $from_bucket_name, 'filename' => $from_object_name),
array('bucket' => $to_bucket_name, 'filename' => $to_object_name )
);
if ($copy_response->isOK())
{
$delete_response = $this->client($protocol)->delete_object($from_bucket_name, $from_object_name);
if ($delete_response->isOK())
{
return true;
}
}
return false;
}
/**
* This method is called in response to <php:rmdir()>.
*
* @param string $path (Required) The bucket name to create.
* @param boolean $context (Optional) Ignored.
* @return boolean Whether the bucket was deleted successfully or not.
*/
public function rmdir($path, $context)
{
$this->path = $path;
list($protocol, $bucket, $object_name) = $this->parse_path($path);
$response = $this->client($protocol)->delete_bucket($bucket);
return $response->isOK();
}
/**
* NOT IMPLEMENTED!
*
* @param integer $cast_as
* @return resource
*/
// public function stream_cast($cast_as) {}
/**
* Close a resource. This method is called in response to <php:fclose()>.
*
* All resources that were locked, or allocated, by the wrapper should be released.
*
* @return void
*/
public function stream_close()
{
$this->position = 0;
$this->path = null;
$this->file_list = null;
$this->open_file = null;
$this->seek_position = 0;
$this->eof = false;
$this->buffer = null;
$this->object_size = 0;
}
/**
* Tests for end-of-file on a file pointer. This method is called in response to <php:feof()>.
*
* @return boolean
*/
public function stream_eof()
{
return $this->eof;
}
/**
* Flushes the output. This method is called in response to <php:fflush()>. If you have cached data in
* your stream but not yet stored it into the underlying storage, you should do so now.
*
* Since this implementation doesn't buffer streams, simply return <code>true</code>.
*
* @return boolean Whether or not flushing succeeded
*/
public function stream_flush()
{
if ($this->buffer === null)
{
return false;
}
list($protocol, $bucket, $object_name) = $this->parse_path($this->path);
$response = $this->client($protocol)->create_object($bucket, $object_name, array(
'body' => $this->buffer,
));
$this->seek_position = 0;
$this->buffer = null;
$this->eof = true;
return $response->isOK();
}
/**
* This method is called in response to <php:flock()>, when <php:file_put_contents()> (when flags contains
* <code>LOCK_EX</code>), <php:stream_set_blocking()> and when closing the stream (<code>LOCK_UN</code>).
*
* Not implemented in S3, so it's not implemented here.
*
* @param mode $operation
* @return boolean
*/
// public function stream_lock($operation) {}
/**
* Opens file or URL. This method is called immediately after the wrapper is initialized
* (e.g., by <php:fopen()> and <php:file_get_contents()>).
*
* @param string $path (Required) Specifies the URL that was passed to the original function.
* @param string $mode (Required) Ignored.
* @param integer $options (Required) Ignored.
* @param string &$opened_path (Required) Returns the same value as was passed into <code>$path</code>.
* @return boolean Returns <code>true</code> on success or <code>false</code> on failure.
*/
public function stream_open($path, $mode, $options, &$opened_path)
{
$opened_path = $path;
$this->open_file = $path;
$this->path = $path;
$this->seek_position = 0;
$this->object_size = 0;
return true;
}
/**
* Read from stream. This method is called in response to <php:fread()> and <php:fgets()>.
*
*
*
* It is important to avoid reading files that are near to or larger than the amount of memory
* allocated to PHP, otherwise "out of memory" errors will occur.
*
* @param integer $count (Required) Always equal to 8192. PHP is fun, isn't it?
* @return string The contents of the Amazon S3 object.
*/
public function stream_read($count)
{
if ($this->eof)
{
return false;
}
list($protocol, $bucket, $object_name) = $this->parse_path($this->path);
if ($this->seek_position > 0 && $this->object_size)
{
if ($count + $this->seek_position > $this->object_size)
{
$count = $this->object_size - $this->seek_position;
}
$start = $this->seek_position;
$end = $this->seek_position + $count;
$response = $this->client($protocol)->get_object($bucket, $object_name, array(
'range' => $start . '-' . $end
));
}
else
{
$response = $this->client($protocol)->get_object($bucket, $object_name);
$this->object_size = isset($response->header['content-length']) ? $response->header['content-length'] : 0;
}
if (!$response->isOK())
{
return false;
}
$data = substr($response->body, 0, min($count, $this->object_size));
$this->seek_position += strlen($data);
if ($this->seek_position >= $this->object_size)
{
$this->eof = true;
$this->seek_position = 0;
$this->object_size = 0;
}
return $data;
}
/**
* Seeks to specific location in a stream. This method is called in response to <php:fseek()>. The read/write
* position of the stream should be updated according to the <code>$offset</code> and <code>$whence</code>
* parameters.
*
* @param integer $offset (Required) The number of bytes to offset from the start of the file.
* @param integer $whence (Optional) Ignored. Always uses <code>SEEK_SET</code>.
* @return boolean Whether or not the seek was successful.
*/
public function stream_seek($offset, $whence)
{
$this->seek_position = $offset;
return true;
}
/**
* @param integer $option
* @param integer $arg1
* @param integer $arg2
* @return boolean
*/
// public function stream_set_option($option, $arg1, $arg2) {}
/**
* Retrieve information about a file resource.
*
* @return array Returns the same data as a call to <php:stat()>.
*/
public function stream_stat()
{
return $this->url_stat($this->path, null);
}
/**
* Retrieve the current position of a stream. This method is called in response to <php:ftell()>.
*
* @return integer Returns the current position of the stream.
*/
public function stream_tell()
{
return $this->seek_position;
}
/**
* Write to stream. This method is called in response to <php:fwrite()>.
*
* It is important to avoid reading files that are larger than the amount of memory allocated to PHP,
* otherwise "out of memory" errors will occur.
*
* @param string $data (Required) The data to write to the stream.
* @return integer The number of bytes that were written to the stream.
*/
public function stream_write($data)
{
$size = strlen($data);
$this->seek_position = $size;
$this->buffer .= $data;
return $this->seek_position;
}
/**
* Delete a file. This method is called in response to <php:unlink()>.
*
* @param string $path (Required) The file URL which should be deleted.
* @return boolean Returns <code>true</code> on success or <code>false</code> on failure.
*/
public function unlink($path)
{
$this->path = $path;
list($protocol, $bucket, $object_name) = $this->parse_path($path);
$response = $this->client($protocol)->delete_object($bucket, $object_name);
return $response->isOK();
}
/**
* This method is called in response to all <php:stat()> related functions.
*
* @param string $path (Required) The file path or URL to stat. Note that in the case of a URL, it must be a <code>://</code> delimited URL. Other URL forms are not supported.
* @param integer $flags (Required) Holds additional flags set by the streams API. This implementation ignores all defined flags.
* @return array Should return as many elements as <php:stat()> does. Unknown or unavailable values should be set to a rational value (usually <code>0</code>).
*/
public function url_stat($path, $flags)
{
// Defaults
$out = array();
$out[0] = $out['dev'] = 0;
$out[1] = $out['ino'] = 0;
$out[2] = $out['mode'] = 0;
$out[3] = $out['nlink'] = 0;
$out[4] = $out['uid'] = 0;
$out[5] = $out['gid'] = 0;
$out[6] = $out['rdev'] = 0;
$out[7] = $out['size'] = 0;
$out[8] = $out['atime'] = 0;
$out[9] = $out['mtime'] = 0;
$out[10] = $out['ctime'] = 0;
$out[11] = $out['blksize'] = 0;
$out[12] = $out['blocks'] = 0;
$this->path = $path;
list($protocol, $bucket, $object_name) = $this->parse_path($this->path);
$file = null;
$mode = 0;
if ($object_name)
{
$response = $this->client($protocol)->list_objects($bucket, array(
'prefix' => $object_name
));
if (!$response->isOK())
{
return $out;
}
// Ummm... yeah...
if (is_object($response->body))
{
$file = $response->body->Contents[0];
}
else
{
$body = simplexml_load_string($response->body);
$file = $body->Contents[0];
}
}
else
{
$response = $this->client($protocol)->list_objects($bucket);
if (!$response->isOK())
{
return $out;
}
}
/*
Type & Permission bitwise values (only those that pertain to S3).
Simulate the concept of a "directory". Nothing has an executable bit because there's no executing on S3.
Reference: http://docstore.mik.ua/orelly/webprog/pcook/ch19_13.htm
0100000 => type: regular file
0040000 => type: directory
0000400 => owner: read permission
0000200 => owner: write permission
0000040 => group: read permission
0000020 => group: write permission
0000004 => others: read permission
0000002 => others: write permission
*/
// File or directory?
// @todo: Add more detailed support for permissions. Currently only takes OWNER into account.
if (!$object_name) // Root of the bucket
{
$mode = octdec('0040777');
}
elseif ($file)
{
$mode = (str_replace('//', '/', $object_name . '/') === (string) $file->Key) ? octdec('0040777') : octdec('0100777'); // Directory, Owner R/W : Regular File, Owner R/W
}
else
{
$mode = octdec('0100777');
}
// Update stat output
$out[2] = $out['mode'] = $mode;
$out[4] = $out['uid'] = (isset($file) ? (string) $file->Owner->ID : 0);
$out[7] = $out['size'] = (isset($file) ? (string) $file->Size : 0);
$out[8] = $out['atime'] = (isset($file) ? date('U', strtotime((string) $file->LastModified)) : 0);
$out[9] = $out['mtime'] = (isset($file) ? date('U', strtotime((string) $file->LastModified)) : 0);
$out[10] = $out['ctime'] = (isset($file) ? date('U', strtotime((string) $file->LastModified)) : 0);
return $out;
}
}
Jump to Line
Something went wrong with that request. Please try again.