S3BucketStreamZip::doesDirectoryExist()   B
last analyzed

Complexity

Conditions 5
Paths 7

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 17
rs 8.8571
cc 5
eloc 10
nc 7
nop 1
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;
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