Subversion-Projekte lars-tiefland.php_share

Revision

Blame | Letzte Änderung | Log anzeigen | RSS feed

<?php
/*
 * Copyright 2011-2012 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;
        }
}