Completed
Push — master ( 25d960...5e7c82 )
by Linus
46:41 queued 36:44
created

S3BucketStreamZip::parts()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
cc 3
eloc 15
nc 3
nop 0
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
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
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