Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/*
3
 * Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License").
6
 * You may not use this file except in compliance with the License.
7
 * A copy of the License is located at
8
 *
9
 *  http://aws.amazon.com/apache2.0
10
 *
11
 * or in the "license" file accompanying this file. This file is distributed
12
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
13
 * express or implied. See the License for the specific language governing
14
 * permissions and limitations under the License.
15
 */
16
 
17
 
18
/**
19
 * Provides an interface for accessing Amazon S3 using PHP's native file management functions.
20
 *
21
 * Amazon S3 file patterns take the following form: <code>s3://bucket/object</code>.
22
 */
23
class S3StreamWrapper
24
{
25
	/**
26
	 * @var array An array of AmazonS3 clients registered as stream wrappers.
27
	 */
28
	protected static $_clients = array();
29
 
30
	/**
31
	 * Registers the S3StreamWrapper class as a stream wrapper.
32
	 *
33
	 * @param AmazonS3 $s3 (Optional) An instance of the AmazonS3 client.
34
	 * @param string $protocol (Optional) The name of the protocol to register.
35
	 * @return boolean Whether or not the registration succeeded.
36
	 */
37
	public static function register(AmazonS3 $s3 = null, $protocol = 's3')
38
	{
39
		S3StreamWrapper::$_clients[$protocol] = $s3 ? $s3 : new AmazonS3();
40
 
41
		return stream_wrapper_register($protocol, 'S3StreamWrapper');
42
	}
43
 
44
	/**
45
	 * Makes the given token PCRE-compatible.
46
	 *
47
	 * @param string $token (Required) The token
48
	 * @return string The PCRE-compatible version of the token
49
	 */
50
	public static function regex_token($token)
51
	{
52
		$token = str_replace('/', '\/', $token);
53
		$token = quotemeta($token);
54
		return str_replace('\\\\', '\\', $token);
55
	}
56
 
57
	public $position = 0;
58
	public $path = null;
59
	public $file_list = null;
60
	public $open_file = null;
61
	public $seek_position = 0;
62
	public $eof = false;
63
	public $buffer = null;
64
	public $object_size = 0;
65
 
66
	/**
67
	 * Fetches the client for the protocol being used.
68
	 *
69
	 * @param string $protocol (Optional) The protocol associated with this stream wrapper.
70
	 * @return AmazonS3 The S3 client associated with this stream wrapper.
71
	 */
72
	public function client($protocol = null)
73
	{
74
		if ($protocol == null)
75
		{
76
			if ($parsed = parse_url($this->path))
77
			{
78
				$protocol = $parsed['scheme'];
79
			}
80
			else
81
			{
82
				trigger_error(__CLASS__ . ' could not determine the protocol of the stream wrapper in use.');
83
			}
84
		}
85
 
86
		return self::$_clients[$protocol];
87
	}
88
 
89
	/**
90
	 * Parses an S3 URL into the parts needed by the stream wrapper.
91
	 *
92
	 * @param string $path The path to parse.
93
	 * @return array An array of 3 items: protocol, bucket, and object name ready for <code>list()</code>.
94
	 */
95
	public function parse_path($path)
96
	{
97
		$url = parse_url($path);
98
 
99
		return array(
100
			$url['scheme'],                                       // Protocol
101
			$url['host'],                                         // Bucket
102
			(isset($url['path']) ? substr($url['path'], 1) : ''), // Object
103
		);
104
	}
105
 
106
	/**
107
	 * Close directory handle. This method is called in response to <php:closedir()>.
108
	 *
109
	 * Since Amazon S3 doesn't have real directories, always return <code>true</code>.
110
	 *
111
	 * @return boolean
112
	 */
113
	public function dir_closedir()
114
	{
115
		$this->position = 0;
116
		$this->path = null;
117
		$this->file_list = null;
118
		$this->open_file = null;
119
		$this->seek_position = 0;
120
		$this->eof = false;
121
		$this->buffer = null;
122
		$this->object_size = 0;
123
 
124
		return true;
125
	}
126
 
127
	/**
128
	 * Open directory handle. This method is called in response to <php:opendir()>.
129
	 *
130
	 * @param string $path (Required) Specifies the URL that was passed to <php:opendir()>.
131
	 * @param integer $options (Required) Not used. Passed in by <php:opendir()>.
132
	 * @return boolean Returns <code>true</code> on success or <code>false</code> on failure.
133
	 */
134
	public function dir_opendir($path, $options)
135
	{
136
		$this->path = $path;
137
		list($protocol, $bucket, $object_name) = $this->parse_path($path);
138
 
139
		$pattern = '/^' . self::regex_token($object_name) . '(.*)[^\/$]/';
140
 
141
		$this->file_list = $this->client($protocol)->get_object_list($bucket, array(
142
			'pcre' => $pattern
143
		));
144
 
145
		return (count($this->file_list)) ? true : false;
146
	}
147
 
148
	/**
149
	 * This method is called in response to <php:readdir()>.
150
	 *
151
	 * @return string Should return a string representing the next filename, or <code>false</code> if there is no next file.
152
	 */
153
	public function dir_readdir()
154
	{
155
		if (isset($this->file_list[$this->position]))
156
		{
157
			$out = $this->file_list[$this->position];
158
			$this->position++;
159
		}
160
		else
161
		{
162
			$out = false;
163
		}
164
 
165
		return $out;
166
	}
167
 
168
	/**
169
	 * This method is called in response to <php:rewinddir()>.
170
	 *
171
	 * Should reset the output generated by <php:streamWrapper::dir_readdir()>. i.e.: The next call to
172
	 * <php:streamWrapper::dir_readdir()> should return the first entry in the location returned by
173
	 * <php:streamWrapper::dir_opendir()>.
174
	 *
175
	 * @return boolean Returns <code>true</code> on success or <code>false</code> on failure.
176
	 */
177
	public function dir_rewinddir()
178
	{
179
		$this->position = 0;
180
 
181
		return true;
182
	}
183
 
184
	/**
185
	 * Create a new bucket. This method is called in response to <php:mkdir()>.
186
	 *
187
	 * @param string $path (Required) The bucket name to create.
188
	 * @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.
189
	 * @param integer $options (Optional) Ignored.
190
	 * @return boolean Whether the bucket was created successfully or not.
191
	 */
192
	public function mkdir($path, $mode, $options)
193
	{
194
		// Get the value that was *actually* passed in as mode, and default to 0
195
		$trace_slice = array_slice(debug_backtrace(), -1);
196
		$mode = isset($trace_slice[0]['args'][1]) ? decoct($trace_slice[0]['args'][1]) : 0;
197
 
198
		$this->path = $path;
199
		list($protocol, $bucket, $object_name) = $this->parse_path($path);
200
 
201
		if (in_array($mode, range(700, 799)))
202
		{
203
			$acl = AmazonS3::ACL_PUBLIC;
204
		}
205
		elseif (in_array($mode, range(600, 699)))
206
		{
207
			$acl = AmazonS3::ACL_AUTH_READ;
208
		}
209
		else
210
		{
211
			$acl = AmazonS3::ACL_PRIVATE;
212
		}
213
 
214
		$client = $this->client($protocol);
215
		$region = $client->hostname;
216
		$response = $client->create_bucket($bucket, $region, $acl);
217
 
218
		return $response->isOK();
219
	}
220
 
221
	/**
222
	 * Renames a file or directory. This method is called in response to <php:rename()>.
223
	 *
224
	 * @param string $path_from (Required) The URL to the current file.
225
	 * @param string $path_to (Required) The URL which the <code>$path_from</code> should be renamed to.
226
	 * @return boolean Returns <code>true</code> on success or <code>false</code> on failure.
227
	 */
228
	public function rename($path_from, $path_to)
229
	{
230
		list($protocol, $from_bucket_name, $from_object_name) = $this->parse_path($path_from);
231
		list($protocol, $to_bucket_name, $to_object_name) = $this->parse_path($path_to);
232
 
233
		$copy_response = $this->client($protocol)->copy_object(
234
			array('bucket' => $from_bucket_name, 'filename' => $from_object_name),
235
			array('bucket' => $to_bucket_name,   'filename' => $to_object_name  )
236
		);
237
 
238
		if ($copy_response->isOK())
239
		{
240
			$delete_response = $this->client($protocol)->delete_object($from_bucket_name, $from_object_name);
241
 
242
			if ($delete_response->isOK())
243
			{
244
				return true;
245
			}
246
		}
247
 
248
		return false;
249
	}
250
 
251
	/**
252
	 * This method is called in response to <php:rmdir()>.
253
	 *
254
	 * @param string $path (Required) The bucket name to create.
255
	 * @param boolean $context (Optional) Ignored.
256
	 * @return boolean Whether the bucket was deleted successfully or not.
257
	 */
258
	public function rmdir($path, $context)
259
	{
260
		$this->path = $path;
261
		list($protocol, $bucket, $object_name) = $this->parse_path($path);
262
 
263
		$response = $this->client($protocol)->delete_bucket($bucket);
264
 
265
		return $response->isOK();
266
	}
267
 
268
	/**
269
	 * NOT IMPLEMENTED!
270
	 *
271
	 * @param integer $cast_as
272
	 * @return resource
273
	 */
274
	// public function stream_cast($cast_as) {}
275
 
276
	/**
277
	 * Close a resource. This method is called in response to <php:fclose()>.
278
	 *
279
	 * All resources that were locked, or allocated, by the wrapper should be released.
280
	 *
281
	 * @return void
282
	 */
283
	public function stream_close()
284
	{
285
		$this->position = 0;
286
		$this->path = null;
287
		$this->file_list = null;
288
		$this->open_file = null;
289
		$this->seek_position = 0;
290
		$this->eof = false;
291
		$this->buffer = null;
292
		$this->object_size = 0;
293
	}
294
 
295
	/**
296
	 * Tests for end-of-file on a file pointer. This method is called in response to <php:feof()>.
297
	 *
298
	 * @return boolean
299
	 */
300
	public function stream_eof()
301
	{
302
		return $this->eof;
303
	}
304
 
305
	/**
306
	 * Flushes the output. This method is called in response to <php:fflush()>. If you have cached data in
307
	 * your stream but not yet stored it into the underlying storage, you should do so now.
308
	 *
309
	 * Since this implementation doesn't buffer streams, simply return <code>true</code>.
310
	 *
311
	 * @return boolean Whether or not flushing succeeded
312
	 */
313
	public function stream_flush()
314
	{
315
		if ($this->buffer === null)
316
		{
317
			return false;
318
		}
319
 
320
		list($protocol, $bucket, $object_name) = $this->parse_path($this->path);
321
 
322
		$response = $this->client($protocol)->create_object($bucket, $object_name, array(
323
			'body' => $this->buffer,
324
		));
325
 
326
		$this->seek_position = 0;
327
		$this->buffer = null;
328
		$this->eof = true;
329
 
330
		return $response->isOK();
331
	}
332
 
333
	/**
334
	 * This method is called in response to <php:flock()>, when <php:file_put_contents()> (when flags contains
335
	 * <code>LOCK_EX</code>), <php:stream_set_blocking()> and when closing the stream (<code>LOCK_UN</code>).
336
	 *
337
	 * Not implemented in S3, so it's not implemented here.
338
	 *
339
	 * @param mode $operation
340
	 * @return boolean
341
	 */
342
	// public function stream_lock($operation) {}
343
 
344
	/**
345
	 * Opens file or URL. This method is called immediately after the wrapper is initialized
346
	 * (e.g., by <php:fopen()> and <php:file_get_contents()>).
347
	 *
348
	 * @param string $path (Required) Specifies the URL that was passed to the original function.
349
	 * @param string $mode (Required) Ignored.
350
	 * @param integer $options (Required) Ignored.
351
	 * @param string &$opened_path (Required) Returns the same value as was passed into <code>$path</code>.
352
	 * @return boolean Returns <code>true</code> on success or <code>false</code> on failure.
353
	 */
354
	public function stream_open($path, $mode, $options, &$opened_path)
355
	{
356
		$opened_path = $path;
357
		$this->open_file = $path;
358
		$this->path = $path;
359
		$this->seek_position = 0;
360
		$this->object_size = 0;
361
 
362
		return true;
363
	}
364
 
365
	/**
366
	 * Read from stream. This method is called in response to <php:fread()> and <php:fgets()>.
367
	 *
368
	 *
369
	 *
370
	 * It is important to avoid reading files that are near to or larger than the amount of memory
371
	 * allocated to PHP, otherwise "out of memory" errors will occur.
372
	 *
373
	 * @param integer $count (Required) Always equal to 8192. PHP is fun, isn't it?
374
	 * @return string The contents of the Amazon S3 object.
375
	 */
376
	public function stream_read($count)
377
	{
378
		if ($this->eof)
379
		{
380
			return false;
381
		}
382
 
383
		list($protocol, $bucket, $object_name) = $this->parse_path($this->path);
384
 
385
		if ($this->seek_position > 0 && $this->object_size)
386
		{
387
			if ($count + $this->seek_position > $this->object_size)
388
			{
389
				$count = $this->object_size - $this->seek_position;
390
			}
391
 
392
			$start = $this->seek_position;
393
			$end = $this->seek_position + $count;
394
 
395
			$response = $this->client($protocol)->get_object($bucket, $object_name, array(
396
				'range' => $start . '-' . $end
397
			));
398
		}
399
		else
400
		{
401
			$response = $this->client($protocol)->get_object($bucket, $object_name);
402
			$this->object_size = isset($response->header['content-length']) ? $response->header['content-length'] : 0;
403
		}
404
 
405
		if (!$response->isOK())
406
		{
407
			return false;
408
		}
409
 
410
		$data = substr($response->body, 0, min($count, $this->object_size));
411
		$this->seek_position += strlen($data);
412
 
413
 
414
		if ($this->seek_position >= $this->object_size)
415
		{
416
			$this->eof = true;
417
			$this->seek_position = 0;
418
			$this->object_size = 0;
419
		}
420
 
421
		return $data;
422
	}
423
 
424
	/**
425
	 * Seeks to specific location in a stream. This method is called in response to <php:fseek()>. The read/write
426
	 * position of the stream should be updated according to the <code>$offset</code> and <code>$whence</code>
427
	 * parameters.
428
	 *
429
	 * @param integer $offset (Required) The number of bytes to offset from the start of the file.
430
	 * @param integer $whence (Optional) Ignored. Always uses <code>SEEK_SET</code>.
431
	 * @return boolean Whether or not the seek was successful.
432
	 */
433
	public function stream_seek($offset, $whence)
434
	{
435
		$this->seek_position = $offset;
436
 
437
		return true;
438
	}
439
 
440
	/**
441
	 * @param integer $option
442
	 * @param integer $arg1
443
	 * @param integer $arg2
444
	 * @return boolean
445
	 */
446
	// public function stream_set_option($option, $arg1, $arg2) {}
447
 
448
	/**
449
	 * Retrieve information about a file resource.
450
	 *
451
	 * @return array Returns the same data as a call to <php:stat()>.
452
	 */
453
	public function stream_stat()
454
	{
455
		return $this->url_stat($this->path, null);
456
	}
457
 
458
	/**
459
	 * Retrieve the current position of a stream. This method is called in response to <php:ftell()>.
460
	 *
461
	 * @return integer Returns the current position of the stream.
462
	 */
463
	public function stream_tell()
464
	{
465
		return $this->seek_position;
466
	}
467
 
468
	/**
469
	 * Write to stream. This method is called in response to <php:fwrite()>.
470
	 *
471
	 * It is important to avoid reading files that are larger than the amount of memory allocated to PHP,
472
	 * otherwise "out of memory" errors will occur.
473
	 *
474
	 * @param string $data (Required) The data to write to the stream.
475
	 * @return integer The number of bytes that were written to the stream.
476
	 */
477
	public function stream_write($data)
478
	{
479
		$size = strlen($data);
480
 
481
		$this->seek_position = $size;
482
		$this->buffer .= $data;
483
 
484
		return $this->seek_position;
485
	}
486
 
487
	/**
488
	 * Delete a file. This method is called in response to <php:unlink()>.
489
	 *
490
	 * @param string $path (Required) The file URL which should be deleted.
491
	 * @return boolean Returns <code>true</code> on success or <code>false</code> on failure.
492
	 */
493
	public function unlink($path)
494
	{
495
		$this->path = $path;
496
		list($protocol, $bucket, $object_name) = $this->parse_path($path);
497
 
498
		$response = $this->client($protocol)->delete_object($bucket, $object_name);
499
 
500
		return $response->isOK();
501
	}
502
 
503
	/**
504
	 * This method is called in response to all <php:stat()> related functions.
505
	 *
506
	 * @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.
507
	 * @param integer $flags (Required) Holds additional flags set by the streams API. This implementation ignores all defined flags.
508
	 * @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>).
509
	 */
510
	public function url_stat($path, $flags)
511
	{
512
		// Defaults
513
		$out = array();
514
		$out[0] = $out['dev'] = 0;
515
		$out[1] = $out['ino'] = 0;
516
		$out[2] = $out['mode'] = 0;
517
		$out[3] = $out['nlink'] = 0;
518
		$out[4] = $out['uid'] = 0;
519
		$out[5] = $out['gid'] = 0;
520
		$out[6] = $out['rdev'] = 0;
521
		$out[7] = $out['size'] = 0;
522
		$out[8] = $out['atime'] = 0;
523
		$out[9] = $out['mtime'] = 0;
524
		$out[10] = $out['ctime'] = 0;
525
		$out[11] = $out['blksize'] = 0;
526
		$out[12] = $out['blocks'] = 0;
527
 
528
		$this->path = $path;
529
		list($protocol, $bucket, $object_name) = $this->parse_path($this->path);
530
 
531
		$file = null;
532
		$mode = 0;
533
 
534
		if ($object_name)
535
		{
536
			$response = $this->client($protocol)->list_objects($bucket, array(
537
				'prefix' => $object_name
538
			));
539
 
540
			if (!$response->isOK())
541
			{
542
				return $out;
543
			}
544
 
545
			// Ummm... yeah...
546
			if (is_object($response->body))
547
			{
548
				$file = $response->body->Contents[0];
549
			}
550
			else
551
			{
552
				$body = simplexml_load_string($response->body);
553
				$file = $body->Contents[0];
554
			}
555
		}
556
		else
557
		{
558
			$response = $this->client($protocol)->list_objects($bucket);
559
 
560
			if (!$response->isOK())
561
			{
562
				return $out;
563
			}
564
		}
565
 
566
		/*
567
		Type & Permission bitwise values (only those that pertain to S3).
568
		Simulate the concept of a "directory". Nothing has an executable bit because there's no executing on S3.
569
		Reference: http://docstore.mik.ua/orelly/webprog/pcook/ch19_13.htm
570
 
571
		0100000 => type:   regular file
572
		0040000 => type:   directory
573
		0000400 => owner:  read permission
574
		0000200 => owner:  write permission
575
		0000040 => group:  read permission
576
		0000020 => group:  write permission
577
		0000004 => others: read permission
578
		0000002 => others: write permission
579
		*/
580
 
581
		// File or directory?
582
		// @todo: Add more detailed support for permissions. Currently only takes OWNER into account.
583
		if (!$object_name) // Root of the bucket
584
		{
585
			$mode = octdec('0040777');
586
		}
587
		elseif ($file)
588
		{
589
			$mode = (str_replace('//', '/', $object_name . '/') === (string) $file->Key) ? octdec('0040777') : octdec('0100777'); // Directory, Owner R/W : Regular File, Owner R/W
590
		}
591
		else
592
		{
593
			$mode = octdec('0100777');
594
		}
595
 
596
		// Update stat output
597
		$out[2] = $out['mode'] = $mode;
598
		$out[4] = $out['uid'] = (isset($file) ? (string) $file->Owner->ID : 0);
599
		$out[7] = $out['size'] = (isset($file) ? (string) $file->Size : 0);
600
		$out[8] = $out['atime'] = (isset($file) ? date('U', strtotime((string) $file->LastModified)) : 0);
601
		$out[9] = $out['mtime'] = (isset($file) ? date('U', strtotime((string) $file->LastModified)) : 0);
602
		$out[10] = $out['ctime'] = (isset($file) ? date('U', strtotime((string) $file->LastModified)) : 0);
603
 
604
		return $out;
605
	}
606
}