Completed
Push — master ( af2d08...69b46f )
by Timur
02:12
created

Container::apiClient()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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\Contracts\FilesTransformerContract;
9
use ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException;
10
use ArgentCrusade\Selectel\CloudStorage\Exceptions\FileNotFoundException;
11
use ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException;
12
use ArgentCrusade\Selectel\CloudStorage\Traits\FilesTransformer;
13
use Countable;
14
use JsonSerializable;
15
use LogicException;
16
17
class Container implements ContainerContract, FilesTransformerContract, Countable, JsonSerializable
18
{
19
    use FilesTransformer;
20
21
    /**
22
     * @var \ArgentCrusade\Selectel\CloudStorage\Contracts\Api\ApiClientContract $api
23
     */
24
    protected $api;
25
26
    /**
27
     * Container name.
28
     *
29
     * @var string
30
     */
31
    protected $containerName;
32
33
    /**
34
     * Container data.
35
     *
36
     * @var array
37
     */
38
    protected $data = [];
39
40
    /**
41
     * Determines if container data was already loaded.
42
     *
43
     * @var bool
44
     */
45
    protected $dataLoaded = false;
46
47
    /**
48
     * @param \ArgentCrusade\Selectel\CloudStorage\Contracts\Api\ApiClientContract $api
49
     * @param string                                                               $name
50
     * @param array                                                                $data
51
     */
52
    public function __construct(ApiClientContract $api, $name, array $data = [])
53
    {
54
        $this->api = $api;
55
        $this->containerName = $name;
56
        $this->data = $data;
57
        $this->dataLoaded = !empty($data);
58
    }
59
60
    /**
61
     * Returns specific container data.
62
     *
63
     * @param string $key
64
     * @param mixed  $default = null
65
     *
66
     * @return mixed
67
     */
68
    protected function containerData($key, $default = null)
69
    {
70
        if (!$this->dataLoaded) {
71
            $this->loadContainerData();
72
        }
73
74
        return isset($this->data[$key]) ? $this->data[$key] : $default;
75
    }
76
77
    /**
78
     * Lazy loading for container data.
79
     *
80
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
81
     */
82
    protected function loadContainerData()
83
    {
84
        // CloudStorage::containers and CloudStorage::getContainer methods did not
85
        // produce any requests to Selectel API, since it may be unnecessary if
86
        // user only wants to upload/manage files or delete container via API.
87
88
        // If user really wants some container info, we will load
89
        // it here on demand. This speeds up application a bit.
90
91
        $response = $this->api->request('HEAD', $this->absolutePath());
92
93
        if ($response->getStatusCode() !== 204) {
94
            throw new ApiRequestFailedException('Container "'.$this->name().'" was not found.');
95
        }
96
97
        $this->dataLoaded = true;
98
        $this->data = [
99
            'type' => $response->getHeaderLine('X-Container-Meta-Type'),
100
            'count' => intval($response->getHeaderLine('X-Container-Object-Count')),
101
            'bytes' => intval($response->getHeaderLine('X-Container-Bytes-Used')),
102
            'rx_bytes' => intval($response->getHeaderLine('X-Received-Bytes')),
103
            'tx_bytes' => intval($response->getHeaderLine('X-Transfered-Bytes')),
104
        ];
105
    }
106
107
    /**
108
     * Absolute path to file from storage root.
109
     *
110
     * @param string $path = '' Relative file path.
111
     *
112
     * @return string
113
     */
114
    protected function absolutePath($path = '')
115
    {
116
        return '/'.$this->name().($path ? '/'.ltrim($path, '/') : '');
117
    }
118
119
    /**
120
     * API Client.
121
     *
122
     * @return \ArgentCrusade\Selectel\CloudStorage\Contracts\Api\ApiClientContract
123
     */
124
    public function apiClient()
125
    {
126
        return $this->api;
127
    }
128
129
    /**
130
     * Returns JSON representation of container.
131
     *
132
     * @return array
133
     */
134
    public function jsonSerialize()
135
    {
136
        return [
137
            'name' => $this->name(),
138
            'type' => $this->type(),
139
            'files_count' => $this->filesCount(),
140
            'size' => $this->size(),
141
            'uploaded_bytes' => $this->uploadedBytes(),
142
            'downloaded_bytes' => $this->downloadedBytes(),
143
        ];
144
    }
145
146
    /**
147
     * Container name.
148
     *
149
     * @return string
150
     */
151
    public function name()
152
    {
153
        return $this->containerName();
154
    }
155
156
    /**
157
     * Container name.
158
     *
159
     * @return string
160
     */
161
    public function containerName()
162
    {
163
        return $this->containerName;
164
    }
165
166
    /**
167
     * Container visibility type.
168
     *
169
     * @return string
170
     */
171
    public function type()
172
    {
173
        return $this->containerData('type', 'public');
174
    }
175
176
    /**
177
     * Container files count.
178
     *
179
     * @return int
180
     */
181
    public function filesCount()
182
    {
183
        return intval($this->containerData('count', 0));
184
    }
185
186
    /**
187
     * Container files count.
188
     *
189
     * @return int
190
     */
191
    public function count()
192
    {
193
        return $this->filesCount();
194
    }
195
196
    /**
197
     * Container size in bytes.
198
     *
199
     * @return int
200
     */
201
    public function size()
202
    {
203
        return intval($this->containerData('bytes', 0));
204
    }
205
206
    /**
207
     * Total uploaded (received) bytes.
208
     *
209
     * @return int
210
     */
211
    public function uploadedBytes()
212
    {
213
        return intval($this->containerData('rx_bytes', 0));
214
    }
215
216
    /**
217
     * Total downloaded (transmitted) bytes.
218
     *
219
     * @return int
220
     */
221
    public function downloadedBytes()
222
    {
223
        return intval($this->containerData('tx_bytes', 0));
224
    }
225
226
    /**
227
     * Determines if container is public.
228
     *
229
     * @return bool
230
     */
231
    public function isPublic()
232
    {
233
        return $this->type() == 'public';
234
    }
235
236
    /**
237
     * Determines if container is private.
238
     *
239
     * @return bool
240
     */
241
    public function isPrivate()
242
    {
243
        return $this->type() == 'private';
244
    }
245
246
    /**
247
     * Determines if container is a gallery container.
248
     *
249
     * @return bool
250
     */
251
    public function isGallery()
252
    {
253
        return $this->type() == 'gallery';
254
    }
255
256
    /**
257
     * Sets container type to 'public'.
258
     *
259
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
260
     *
261
     * @return string
262
     */
263
    public function setPublic()
264
    {
265
        return $this->setType('public');
266
    }
267
268
    /**
269
     * Sets container type to 'private'.
270
     *
271
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
272
     *
273
     * @return string
274
     */
275
    public function setPrivate()
276
    {
277
        return $this->setType('private');
278
    }
279
280
    /**
281
     * Sets container type to 'gallery'.
282
     *
283
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
284
     *
285
     * @return string
286
     */
287
    public function setGallery()
288
    {
289
        return $this->setType('gallery');
290
    }
291
292
    /**
293
     * Updates container type.
294
     *
295
     * @param string $type Container type, 'public', 'private' or 'gallery'.
296
     *
297
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
298
     *
299
     * @return string
300
     */
301
    protected function setType($type)
302
    {
303
        if ($this->type() === $type) {
304
            return $type;
305
        }
306
307
        $response = $this->api->request('POST', $this->absolutePath(), [
308
            'headers' => [
309
                'X-Container-Meta-Type' => $type,
310
            ],
311
        ]);
312
313
        if ($response->getStatusCode() !== 202) {
314
            throw new ApiRequestFailedException('Unable to set container type to "'.$type.'".', $response->getStatusCode());
315
        }
316
317
        return $this->data['type'] = $type;
318
    }
319
320
    /**
321
     * Creates new Fluent files loader instance.
322
     *
323
     * @return \ArgentCrusade\Selectel\CloudStorage\Contracts\FluentFilesLoaderContract
324
     */
325
    public function files()
326
    {
327
        return new FluentFilesLoader($this->api, $this->name(), $this->absolutePath());
328
    }
329
330
    /**
331
     * Determines whether file exists or not.
332
     *
333
     * @param string $path File path.
334
     *
335
     * @return bool
336
     */
337
    public function fileExists($path)
338
    {
339
        return $this->files()->exists($path);
340
    }
341
342
    /**
343
     * Retrieves file object container. This method does not actually download file, see File::read or File::readStream.
344
     *
345
     * @param string $path
346
     *
347
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\FileNotFoundException
348
     *
349
     * @return \ArgentCrusade\Selectel\CloudStorage\Contracts\FileContract
350
     */
351
    public function getFile($path)
352
    {
353
        return $this->files()->find($path);
354
    }
355
356
    /**
357
     * Creates new directory.
358
     *
359
     * @param string $name Directory name.
360
     *
361
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
362
     *
363
     * @return string
364
     */
365
    public function createDir($name)
366
    {
367
        $response = $this->api->request('PUT', $this->absolutePath($name), [
368
            'headers' => [
369
                'Content-Type' => 'application/directory',
370
            ],
371
        ]);
372
373
        if ($response->getStatusCode() !== 201) {
374
            throw new ApiRequestFailedException('Unable to create directory "'.$name.'".', $response->getStatusCode());
375
        }
376
377
        return $response->getHeaderLine('ETag');
378
    }
379
380
    /**
381
     * Deletes directory.
382
     *
383
     * @param string $name Directory name.
384
     */
385
    public function deleteDir($name)
386
    {
387
        $response = $this->api->request('DELETE', $this->absolutePath($name));
388
389
        if ($response->getStatusCode() !== 204) {
390
            throw new ApiRequestFailedException('Unable to delete directory "'.$name.'".', $response->getStatusCode());
391
        }
392
393
        return true;
394
    }
395
396
    /**
397
     * Uploads file contents from string. Returns ETag header value if upload was successful.
398
     *
399
     * @param string $path           Remote path.
400
     * @param string $contents       File contents.
401
     * @param array  $params         = [] Upload params.
402
     * @param bool   $verifyChecksum = true
403
     *
404
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException
405
     *
406
     * @return string
407
     */
408
    public function uploadFromString($path, $contents, array $params = [], $verifyChecksum = true)
409
    {
410
        return $this->uploadFrom($path, $contents, $params, $verifyChecksum);
411
    }
412
413
    /**
414
     * Uploads file from stream. Returns ETag header value if upload was successful.
415
     *
416
     * @param string   $path     Remote path.
417
     * @param resource $resource Stream resource.
418
     * @param array    $params   = [] Upload params.
419
     *
420
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException
421
     *
422
     * @return string
423
     */
424
    public function uploadFromStream($path, $resource, array $params = [])
425
    {
426
        return $this->uploadFrom($path, $resource, $params, false);
427
    }
428
429
    /**
430
     * Upload file from string or stream resource.
431
     *
432
     * @param string            $path           Remote path.
433
     * @param string | resource $contents       File contents.
434
     * @param array             $params         = [] Upload params.
435
     * @param bool              $verifyChecksum = true
436
     *
437
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException
438
     *
439
     * @return string
440
     */
441
    protected function uploadFrom($path, $contents, array $params = [], $verifyChecksum = true)
442
    {
443
        $response = $this->api->request('PUT', $this->absolutePath($path), [
444
            'headers' => $this->convertUploadParamsToHeaders($contents, $params, $verifyChecksum),
445
            'body' => $contents,
446
        ]);
447
448
        if ($response->getStatusCode() !== 201) {
449
            throw new UploadFailedException('Unable to upload file.', $response->getStatusCode());
450
        }
451
452
        return $response->getHeaderLine('ETag');
453
    }
454
455
    /**
456
     * Parses upload parameters and assigns them to appropriate HTTP headers.
457
     *
458
     * @param string $contents       = null
459
     * @param array  $params         = []
460
     * @param bool   $verifyChecksum = true
461
     *
462
     * @return array
463
     */
464
    protected function convertUploadParamsToHeaders($contents = null, array $params = [], $verifyChecksum = true)
465
    {
466
        $headers = [];
467
468
        if ($verifyChecksum) {
469
            $headers['ETag'] = md5($contents);
470
        }
471
472
        $availableParams = [
473
            'contentType' => 'Content-Type',
474
            'contentDisposition' => 'Content-Disposition',
475
            'deleteAfter' => 'X-Delete-After',
476
            'deleteAt' => 'X-Delete-At',
477
        ];
478
479
        foreach ($availableParams as $key => $header) {
480
            if (isset($params[$key])) {
481
                $headers[$header] = $params[$key];
482
            }
483
        }
484
485
        return $headers;
486
    }
487
488
    /**
489
     * Deletes container. Container must be empty in order to perform this operation.
490
     *
491
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
492
     */
493
    public function delete()
494
    {
495
        $response = $this->api->request('DELETE', $this->absolutePath());
496
497
        switch ($response->getStatusCode()) {
498
            case 204:
499
                // Container removed.
500
                return;
501
            case 404:
502
                throw new ApiRequestFailedException('Container "'.$this->name().'" was not found.');
503
            case 409:
504
                throw new ApiRequestFailedException('Container must be empty.');
505
        }
506
    }
507
}
508