Completed
Push — master ( 020561...88353f )
by Timur
01:59
created

Container   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 377
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 8
dl 0
loc 377
rs 8.6
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A containerData() 0 8 3
B loadContainerData() 0 24 2
A name() 0 4 1
A type() 0 4 1
A filesCount() 0 4 1
A count() 0 4 1
A size() 0 4 1
A uploadedBytes() 0 4 1
A downloadedBytes() 0 4 1
A jsonSerialize() 0 11 1
A isPublic() 0 4 1
A isPrivate() 0 4 1
B files() 0 18 5
A getFile() 0 14 3
A uploadFromString() 0 4 1
A uploadFromStream() 0 4 1
A uploadFrom() 0 16 2
B convertUploadParamsToHeaders() 0 23 4
A normalizeUploadPath() 0 4 1
A delete() 0 14 4
1
<?php
2
3
namespace ArgentCrusade\Selectel\CloudStorage;
4
5
use ArgentCrusade\Selectel\CloudStorage\Collections\Collection;
6
use ArgentCrusade\Selectel\CloudStorage\Contracts\Api\ApiClientContract;
7
use ArgentCrusade\Selectel\CloudStorage\Contracts\ContainerContract;
8
use ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException;
9
use ArgentCrusade\Selectel\CloudStorage\Exceptions\FileNotFoundException;
10
use ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException;
11
use Countable;
12
use JsonSerializable;
13
use LogicException;
14
15
class Container implements ContainerContract, Countable, JsonSerializable
16
{
17
    /**
18
     * @var \ArgentCrusade\Selectel\CloudStorage\Contracts\Api\ApiClientContract $api
19
     */
20
    protected $api;
21
22
    /**
23
     * Container name.
24
     *
25
     * @var string
26
     */
27
    protected $name;
28
29
    /**
30
     * Container data.
31
     *
32
     * @var array
33
     */
34
    protected $data = [];
35
36
    /**
37
     * Determines if container data was already loaded.
38
     *
39
     * @var bool
40
     */
41
    protected $dataLoaded = false;
42
43
    /**
44
     * @param \ArgentCrusade\Selectel\CloudStorage\Contracts\Api\ApiClientContract $api
45
     * @param string                                                               $name
46
     * @param array                                                                $data
47
     */
48
    public function __construct(ApiClientContract $api, $name, array $data = [])
49
    {
50
        $this->api = $api;
51
        $this->name = $name;
52
        $this->data = $data;
53
        $this->dataLoaded = !empty($data);
54
    }
55
56
    /**
57
     * Returns specific container data.
58
     *
59
     * @param string $key
60
     * @param mixed  $default = null
61
     *
62
     * @return mixed
63
     */
64
    protected function containerData($key, $default = null)
65
    {
66
        if (!$this->dataLoaded) {
67
            $this->loadContainerData();
68
        }
69
70
        return isset($this->data[$key]) ? $this->data[$key] : $default;
71
    }
72
73
    /**
74
     * Lazy loading for container data.
75
     *
76
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
77
     */
78
    protected function loadContainerData()
79
    {
80
        // CloudStorage::containers and CloudStorage::getContainer methods did not
81
        // produce any requests to Selectel API, since it may be unnecessary if
82
        // user only wants to upload/manage files or delete container via API.
83
84
        // If user really wants some container info, we will load
85
        // it here on demand. This speeds up application a bit.
86
87
        $response = $this->api->request('HEAD', '/'.$this->name());
88
89
        if ($response->getStatusCode() !== 204) {
90
            throw new ApiRequestFailedException('Container "'.$this->name().'" was not found.');
91
        }
92
93
        $this->dataLoaded = true;
94
        $this->data = [
95
            'type' => $response->getHeaderLine('X-Container-Meta-Type'),
96
            'count' => intval($response->getHeaderLine('X-Container-Object-Count')),
97
            'bytes' => intval($response->getHeaderLine('X-Container-Bytes-Used')),
98
            'rx_bytes' => intval($response->getHeaderLine('X-Received-Bytes')),
99
            'tx_bytes' => intval($response->getHeaderLine('X-Transfered-Bytes')),
100
        ];
101
    }
102
103
    /**
104
     * Container name.
105
     *
106
     * @return string
107
     */
108
    public function name()
109
    {
110
        return $this->name;
111
    }
112
113
    /**
114
     * Container visibility type.
115
     *
116
     * @return string
117
     */
118
    public function type()
119
    {
120
        return $this->containerData('type', 'public');
121
    }
122
123
    /**
124
     * Container files count.
125
     *
126
     * @return int
127
     */
128
    public function filesCount()
129
    {
130
        return intval($this->containerData('count', 0));
131
    }
132
133
    /**
134
     * Container files count.
135
     *
136
     * @return int
137
     */
138
    public function count()
139
    {
140
        return $this->filesCount();
141
    }
142
143
    /**
144
     * Container size in bytes.
145
     *
146
     * @return int
147
     */
148
    public function size()
149
    {
150
        return intval($this->containerData('bytes', 0));
151
    }
152
153
    /**
154
     * Total uploaded (received) bytes.
155
     *
156
     * @return int
157
     */
158
    public function uploadedBytes()
159
    {
160
        return intval($this->containerData('rx_bytes', 0));
161
    }
162
163
    /**
164
     * Total downloaded (transmitted) bytes.
165
     *
166
     * @return int
167
     */
168
    public function downloadedBytes()
169
    {
170
        return intval($this->containerData('tx_bytes', 0));
171
    }
172
173
    /**
174
     * Returns JSON representation of container.
175
     *
176
     * @return array
177
     */
178
    public function jsonSerialize()
179
    {
180
        return [
181
            'name' => $this->name(),
182
            'type' => $this->type(),
183
            'files_count' => $this->filesCount(),
184
            'size' => $this->size(),
185
            'uploaded_bytes' => $this->uploadedBytes(),
186
            'downloaded_bytes' => $this->downloadedBytes(),
187
        ];
188
    }
189
190
    /**
191
     * Determines if container is public.
192
     *
193
     * @return bool
194
     */
195
    public function isPublic()
196
    {
197
        return $this->type() == 'public';
198
    }
199
200
    /**
201
     * Determines if container is private.
202
     *
203
     * @return bool
204
     */
205
    public function isPrivate()
206
    {
207
        return !$this->isPublic();
208
    }
209
210
    /**
211
     * Retrieves files from current container.
212
     *
213
     * @param string $directory        = null
214
     * @param string $prefixOrFullPath = null
215
     * @param string $delimiter        = null
216
     * @param int    $limit            = 10000
217
     * @param string $marker           = ''
218
     *
219
     * @return \ArgentCrusade\Selectel\CloudStorage\Contracts\Collections\CollectionContract
220
     */
221
    public function files($directory = null, $prefixOrFullPath = null, $delimiter = null, $limit = 10000, $marker = '')
222
    {
223
        $response = $this->api->request('GET', '/'.$this->name(), [
224
            'query' => [
225
                'limit' => intval($limit),
226
                'marker' => $marker,
227
                'path' => !is_null($directory) ? ltrim($directory, '/') : '',
228
                'prefix' => !is_null($prefixOrFullPath) ? ltrim($prefixOrFullPath, '/') : '',
229
                'delimiter' => !is_null($delimiter) ? $delimiter : '',
230
            ],
231
        ]);
232
233
        if ($response->getStatusCode() !== 200) {
234
            throw new ApiRequestFailedException('Unable to list container files.', $response->getStatusCode());
235
        }
236
237
        return new Collection(json_decode($response->getBody(), true));
238
    }
239
240
    /**
241
     * Retrieves file object container. This method does not actually download file, see File::download.
242
     *
243
     * @param string $path
244
     *
245
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\FileNotFoundException
246
     * @throws \LogicException
247
     *
248
     * @return \ArgentCrusade\Selectel\CloudStorage\Contracts\FileContract
249
     */
250
    public function getFile($path)
251
    {
252
        $files = $this->files(null, $path);
253
254
        if (!count($files)) {
255
            throw new FileNotFoundException('File "'.$path.'" was not found in container "'.$this->name().'".');
256
        }
257
258
        if (count($files) > 1) {
259
            throw new LogicException('There is more than one file that satisfies given path "'.$path.'".');
260
        }
261
262
        return new File($this->api, $this->name(), $files->get(0));
263
    }
264
265
    /**
266
     * Uploads file contents from string. Returns ETag header value if upload was successful.
267
     *
268
     * @param string $path           Remote path.
269
     * @param string $contents       File contents.
270
     * @param array  $params         = [] Upload params.
271
     * @param bool   $verifyChecksum = true
272
     *
273
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException
274
     *
275
     * @return string
276
     */
277
    public function uploadFromString($path, $contents, array $params = [], $verifyChecksum = true)
278
    {
279
        return $this->uploadFrom($path, $contents, $params, $verifyChecksum);
280
    }
281
282
    /**
283
     * Uploads file from stream. Returns ETag header value if upload was successful.
284
     *
285
     * @param string   $path     Remote path.
286
     * @param resource $resource Stream resource.
287
     * @param array    $params   = [] Upload params.
288
     *
289
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException
290
     *
291
     * @return string
292
     */
293
    public function uploadFromStream($path, $resource, array $params = [])
294
    {
295
        return $this->uploadFrom($path, $resource, $params, false);
296
    }
297
298
    /**
299
     * Upload file from string or stream resource.
300
     *
301
     * @param string            $path           Remote path.
302
     * @param string | resource $contents       File contents.
303
     * @param array             $params         = [] Upload params.
304
     * @param bool              $verifyChecksum = true
305
     *
306
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException
307
     *
308
     * @return string
309
     */
310
    protected function uploadFrom($path, $contents, array $params = [], $verifyChecksum = true)
311
    {
312
        $headers = $this->convertUploadParamsToHeaders($contents, $params, $verifyChecksum);
313
        $url = $this->normalizeUploadPath($path);
314
315
        $response = $this->api->request('PUT', $url, [
316
            'headers' => $headers,
317
            'body' => $contents,
318
        ]);
319
320
        if ($response->getStatusCode() !== 201) {
321
            throw new UploadFailedException('Unable to upload file.', $response->getStatusCode());
322
        }
323
324
        return $response->getHeaderLine('ETag');
325
    }
326
327
    /**
328
     * Parses upload parameters and assigns them to appropriate HTTP headers.
329
     *
330
     * @param string $contents       = null
331
     * @param array  $params         = []
332
     * @param bool   $verifyChecksum = true
333
     *
334
     * @return array
335
     */
336
    protected function convertUploadParamsToHeaders($contents = null, array $params = [], $verifyChecksum = true)
337
    {
338
        $headers = [];
339
340
        if ($verifyChecksum) {
341
            $headers['ETag'] = md5($contents);
342
        }
343
344
        $availableParams = [
345
            'contentType' => 'Content-Type',
346
            'contentDisposition' => 'Content-Disposition',
347
            'deleteAfter' => 'X-Delete-After',
348
            'deleteAt' => 'X-Delete-At',
349
        ];
350
351
        foreach ($availableParams as $key => $header) {
352
            if (isset($params[$key])) {
353
                $headers[$header] = $params[$key];
354
            }
355
        }
356
357
        return $headers;
358
    }
359
360
    /**
361
     * Normalizes upload path.
362
     *
363
     * @param string $path Remote path (without container name).
364
     *
365
     * @return string
366
     */
367
    protected function normalizeUploadPath($path)
368
    {
369
        return '/'.$this->name().'/'.ltrim($path, '/');
370
    }
371
372
    /**
373
     * Deletes container. Container must be empty in order to perform this operation.
374
     *
375
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
376
     */
377
    public function delete()
378
    {
379
        $response = $this->api->request('DELETE', '/'.$this->name());
380
381
        switch ($response->getStatusCode()) {
382
            case 204:
383
                // Container removed.
384
                return;
385
            case 404:
386
                throw new ApiRequestFailedException('Container "'.$this->name().'" was not found.');
387
            case 409:
388
                throw new ApiRequestFailedException('Container must be empty.');
389
        }
390
    }
391
}
392