1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace limenet\S3BucketStreamZip; |
4
|
|
|
|
5
|
|
|
use Aws\S3\Exception\S3Exception; |
6
|
|
|
use Aws\S3\S3Client; |
7
|
|
|
use limenet\S3BucketStreamZip\Exception\InvalidParameterException; |
8
|
|
|
use ZipStream\ZipStream; |
9
|
|
|
|
10
|
|
|
class S3BucketStreamZip |
11
|
|
|
{ |
12
|
|
|
public const MAX_ARCHIVE_SIZE = 1073741824; // 2^30 * 2 = 2 GB |
|
|
|
|
13
|
|
|
|
14
|
|
|
protected $auth = []; |
15
|
|
|
|
16
|
|
|
protected $s3Client; |
17
|
|
|
|
18
|
|
|
protected $params; |
19
|
|
|
|
20
|
|
|
protected $part; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* Create a new ZipStream object. |
24
|
|
|
* |
25
|
|
|
* @param array $auth - AWS key and secret |
26
|
|
|
* |
27
|
|
|
* @throws InvalidParameterException |
28
|
|
|
*/ |
29
|
|
|
public function __construct($auth, $part = 0) |
30
|
|
|
{ |
31
|
|
|
$this->validateAuth($auth); |
32
|
|
|
|
33
|
|
|
$this->auth = $auth; |
34
|
|
|
$this->part = $part; |
35
|
|
|
|
36
|
|
|
// S3 User in $this->auth should have permission to execute ListBucket on any buckets |
37
|
|
|
// AND GetObject on any object with which you need to interact. |
38
|
|
|
$this->s3Client = new S3Client([ |
39
|
|
|
'version' => (isset($this->auth['version'])) ? $this->auth['version'] : 'latest', |
40
|
|
|
'region' => (isset($this->auth['region'])) ? $this->auth['region'] : 'us-east-1', |
41
|
|
|
'credentials' => [ |
42
|
|
|
'key' => $this->auth['key'], |
43
|
|
|
'secret' => $this->auth['secret'], |
44
|
|
|
], |
45
|
|
|
]); |
46
|
|
|
|
47
|
|
|
// Register the stream wrapper from an S3Client object |
48
|
|
|
// This allows you to access buckets and objects stored in Amazon S3 using the s3:// protocol |
49
|
|
|
$this->s3Client->registerStreamWrapper(); |
50
|
|
|
} |
51
|
|
|
|
52
|
|
|
public function bucket($bucket) |
53
|
|
|
{ |
54
|
|
|
$this->params = new S3Params($bucket); |
55
|
|
|
|
56
|
|
|
return $this; |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
public function prefix($prefix) |
60
|
|
|
{ |
61
|
|
|
$this->params->setParam('Prefix', rtrim($prefix, '/').'/'); |
62
|
|
|
|
63
|
|
|
return $this; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
public function addParams(array $params) |
67
|
|
|
{ |
68
|
|
|
foreach ($params as $key => $value) { |
69
|
|
|
$this->params->setParam($key, $value); |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
return $this; |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Stream a zip file to the client. |
77
|
|
|
* |
78
|
|
|
* @param string $filename - Name for the file to be sent to the client |
79
|
|
|
* $filename will be what is sent in the content-disposition header |
80
|
|
|
* |
81
|
|
|
* @throws InvalidParameterException |
82
|
|
|
* |
83
|
|
|
* @internal param array - See the documentation for the List Objects API for valid parameters. |
84
|
|
|
* Only `Bucket` is required. |
85
|
|
|
* |
86
|
|
|
* http://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketGET.html |
87
|
|
|
*/ |
88
|
|
|
public function send($filename) |
89
|
|
|
{ |
90
|
|
|
$params = $this->params->getParams(); |
91
|
|
|
|
92
|
|
|
$this->doesDirectoryExist($params); |
93
|
|
|
|
94
|
|
|
$zip = new ZipStream($filename); |
95
|
|
|
|
96
|
|
|
$parts = $this->parts(); |
97
|
|
|
|
98
|
|
|
// Add each object from the ListObjects call to the new zip file. |
99
|
|
|
foreach ($parts[$this->part] as $file) { |
100
|
|
|
// Get the file name on S3 so we can save it to the zip file using the same name. |
101
|
|
|
$fileName = basename($file['Key']); |
102
|
|
|
|
103
|
|
|
if (is_file("s3://{$params['Bucket']}/{$file['Key']}")) { |
104
|
|
|
$context = stream_context_create([ |
105
|
|
|
's3' => ['seekable' => true], |
106
|
|
|
]); |
107
|
|
|
// open seekable(!) stream |
108
|
|
|
if ($stream = fopen("s3://{$params['Bucket']}/{$file['Key']}", 'r', false, $context)) { |
109
|
|
|
$zip->addFileFromStream($fileName, $stream); |
110
|
|
|
} |
111
|
|
|
} |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
// Finalize the zip file. |
115
|
|
|
$zip->finish(); |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
public function parts() |
119
|
|
|
{ |
120
|
|
|
$params = $this->params->getParams(); |
121
|
|
|
|
122
|
|
|
$this->doesDirectoryExist($params); |
123
|
|
|
|
124
|
|
|
// The iterator fetches ALL of the objects without having to manually loop over responses. |
125
|
|
|
$files = $this->s3Client->getIterator('ListObjects', $params); |
126
|
|
|
|
127
|
|
|
$parts = [0 => []]; |
128
|
|
|
$partSizes = [0 => 0]; |
129
|
|
|
$currentPart = 0; |
130
|
|
|
foreach ($files as $file) { |
131
|
|
|
if ($partSizes[$currentPart] + $file['Size'] > self::MAX_ARCHIVE_SIZE) { |
132
|
|
|
$currentPart++; |
133
|
|
|
$parts[$currentPart] = []; |
134
|
|
|
$partSizes[$currentPart] = 0; |
135
|
|
|
} |
136
|
|
|
$parts[$currentPart][] = $file; |
137
|
|
|
$partSizes[$currentPart] += $file['Size']; |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
return $parts; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
protected function validateAuth($auth) |
144
|
|
|
{ |
145
|
|
|
// We require the AWS key to be passed in $auth. |
146
|
|
|
if (!isset($auth['key'])) { |
147
|
|
|
throw new InvalidParameterException('$auth parameter to constructor requires a `key` attribute'); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
// We require the AWS secret to be passed in $auth. |
151
|
|
|
if (!isset($auth['secret'])) { |
152
|
|
|
throw new InvalidParameterException('$auth parameter to constructor requires a `secret` attribute'); |
153
|
|
|
} |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
protected function doesDirectoryExist($params) |
157
|
|
|
{ |
158
|
|
|
$command = $this->s3Client->getCommand('listObjects', $params); |
159
|
|
|
|
160
|
|
|
try { |
161
|
|
|
$result = $this->s3Client->execute($command); |
162
|
|
|
|
163
|
|
|
if (empty($result['Contents']) && empty($result['CommonPrefixes'])) { |
164
|
|
|
throw new InvalidParameterException('Bucket or Prefix does not exist'); |
165
|
|
|
} |
166
|
|
|
} catch (S3Exception $e) { |
167
|
|
|
if ($e->getStatusCode() === 403) { |
168
|
|
|
return false; |
169
|
|
|
} |
170
|
|
|
throw $e; |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
} |
174
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.