Completed
Push — master ( 8ae150...af2d08 )
by Timur
02:28
created

Container::files()   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
     * Returns JSON representation of container.
121
     *
122
     * @return array
123
     */
124
    public function jsonSerialize()
125
    {
126
        return [
127
            'name' => $this->name(),
128
            'type' => $this->type(),
129
            'files_count' => $this->filesCount(),
130
            'size' => $this->size(),
131
            'uploaded_bytes' => $this->uploadedBytes(),
132
            'downloaded_bytes' => $this->downloadedBytes(),
133
        ];
134
    }
135
136
    /**
137
     * Container name.
138
     *
139
     * @return string
140
     */
141
    public function name()
142
    {
143
        return $this->containerName();
144
    }
145
146
    /**
147
     * Container name.
148
     *
149
     * @return string
150
     */
151
    public function containerName()
152
    {
153
        return $this->containerName;
154
    }
155
156
    /**
157
     * Container visibility type.
158
     *
159
     * @return string
160
     */
161
    public function type()
162
    {
163
        return $this->containerData('type', 'public');
164
    }
165
166
    /**
167
     * Container files count.
168
     *
169
     * @return int
170
     */
171
    public function filesCount()
172
    {
173
        return intval($this->containerData('count', 0));
174
    }
175
176
    /**
177
     * Container files count.
178
     *
179
     * @return int
180
     */
181
    public function count()
182
    {
183
        return $this->filesCount();
184
    }
185
186
    /**
187
     * Container size in bytes.
188
     *
189
     * @return int
190
     */
191
    public function size()
192
    {
193
        return intval($this->containerData('bytes', 0));
194
    }
195
196
    /**
197
     * Total uploaded (received) bytes.
198
     *
199
     * @return int
200
     */
201
    public function uploadedBytes()
202
    {
203
        return intval($this->containerData('rx_bytes', 0));
204
    }
205
206
    /**
207
     * Total downloaded (transmitted) bytes.
208
     *
209
     * @return int
210
     */
211
    public function downloadedBytes()
212
    {
213
        return intval($this->containerData('tx_bytes', 0));
214
    }
215
216
    /**
217
     * Determines if container is public.
218
     *
219
     * @return bool
220
     */
221
    public function isPublic()
222
    {
223
        return $this->type() == 'public';
224
    }
225
226
    /**
227
     * Determines if container is private.
228
     *
229
     * @return bool
230
     */
231
    public function isPrivate()
232
    {
233
        return $this->type() == 'private';
234
    }
235
236
    /**
237
     * Determines if container is a gallery container.
238
     *
239
     * @return bool
240
     */
241
    public function isGallery()
242
    {
243
        return $this->type() == 'gallery';
244
    }
245
246
    /**
247
     * Sets container type to 'public'.
248
     *
249
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
250
     *
251
     * @return string
252
     */
253
    public function setPublic()
254
    {
255
        return $this->setType('public');
256
    }
257
258
    /**
259
     * Sets container type to 'private'.
260
     *
261
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
262
     *
263
     * @return string
264
     */
265
    public function setPrivate()
266
    {
267
        return $this->setType('private');
268
    }
269
270
    /**
271
     * Sets container type to 'gallery'.
272
     *
273
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
274
     *
275
     * @return string
276
     */
277
    public function setGallery()
278
    {
279
        return $this->setType('gallery');
280
    }
281
282
    /**
283
     * Updates container type.
284
     *
285
     * @param string $type Container type, 'public', 'private' or 'gallery'.
286
     *
287
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
288
     *
289
     * @return string
290
     */
291
    protected function setType($type)
292
    {
293
        if ($this->type() === $type) {
294
            return $type;
295
        }
296
297
        $response = $this->api->request('POST', $this->absolutePath(), [
298
            'headers' => [
299
                'X-Container-Meta-Type' => $type,
300
            ],
301
        ]);
302
303
        if ($response->getStatusCode() !== 202) {
304
            throw new ApiRequestFailedException('Unable to set container type to "'.$type.'".', $response->getStatusCode());
305
        }
306
307
        return $this->data['type'] = $type;
308
    }
309
310
    /**
311
     * Creates new Fluent files loader instance.
312
     *
313
     * @return \ArgentCrusade\Selectel\CloudStorage\Contracts\FluentFilesLoaderContract
314
     */
315
    public function files()
316
    {
317
        return new FluentFilesLoader($this->api, $this->name(), $this->absolutePath());
318
    }
319
320
    /**
321
     * Determines whether file exists or not.
322
     *
323
     * @param string $path File path.
324
     *
325
     * @return bool
326
     */
327
    public function fileExists($path)
328
    {
329
        return $this->files()->exists($path);
330
    }
331
332
    /**
333
     * Retrieves file object container. This method does not actually download file, see File::read or File::readStream.
334
     *
335
     * @param string $path
336
     *
337
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\FileNotFoundException
338
     *
339
     * @return \ArgentCrusade\Selectel\CloudStorage\Contracts\FileContract
340
     */
341
    public function getFile($path)
342
    {
343
        return $this->files()->find($path);
344
    }
345
346
    /**
347
     * Creates new directory.
348
     *
349
     * @param string $name Directory name.
350
     *
351
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
352
     *
353
     * @return string
354
     */
355
    public function createDir($name)
356
    {
357
        $response = $this->api->request('PUT', $this->absolutePath($name), [
358
            'headers' => [
359
                'Content-Type' => 'application/directory',
360
            ],
361
        ]);
362
363
        if ($response->getStatusCode() !== 201) {
364
            throw new ApiRequestFailedException('Unable to create directory "'.$name.'".', $response->getStatusCode());
365
        }
366
367
        return $response->getHeaderLine('ETag');
368
    }
369
370
    /**
371
     * Deletes directory.
372
     *
373
     * @param string $name Directory name.
374
     */
375
    public function deleteDir($name)
376
    {
377
        $response = $this->api->request('DELETE', $this->absolutePath($name));
378
379
        if ($response->getStatusCode() !== 204) {
380
            throw new ApiRequestFailedException('Unable to delete directory "'.$name.'".', $response->getStatusCode());
381
        }
382
383
        return true;
384
    }
385
386
    /**
387
     * Uploads file contents from string. Returns ETag header value if upload was successful.
388
     *
389
     * @param string $path           Remote path.
390
     * @param string $contents       File contents.
391
     * @param array  $params         = [] Upload params.
392
     * @param bool   $verifyChecksum = true
393
     *
394
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException
395
     *
396
     * @return string
397
     */
398
    public function uploadFromString($path, $contents, array $params = [], $verifyChecksum = true)
399
    {
400
        return $this->uploadFrom($path, $contents, $params, $verifyChecksum);
401
    }
402
403
    /**
404
     * Uploads file from stream. Returns ETag header value if upload was successful.
405
     *
406
     * @param string   $path     Remote path.
407
     * @param resource $resource Stream resource.
408
     * @param array    $params   = [] Upload params.
409
     *
410
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException
411
     *
412
     * @return string
413
     */
414
    public function uploadFromStream($path, $resource, array $params = [])
415
    {
416
        return $this->uploadFrom($path, $resource, $params, false);
417
    }
418
419
    /**
420
     * Upload file from string or stream resource.
421
     *
422
     * @param string            $path           Remote path.
423
     * @param string | resource $contents       File contents.
424
     * @param array             $params         = [] Upload params.
425
     * @param bool              $verifyChecksum = true
426
     *
427
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException
428
     *
429
     * @return string
430
     */
431
    protected function uploadFrom($path, $contents, array $params = [], $verifyChecksum = true)
432
    {
433
        $response = $this->api->request('PUT', $this->absolutePath($path), [
434
            'headers' => $this->convertUploadParamsToHeaders($contents, $params, $verifyChecksum),
435
            'body' => $contents,
436
        ]);
437
438
        if ($response->getStatusCode() !== 201) {
439
            throw new UploadFailedException('Unable to upload file.', $response->getStatusCode());
440
        }
441
442
        return $response->getHeaderLine('ETag');
443
    }
444
445
    /**
446
     * Parses upload parameters and assigns them to appropriate HTTP headers.
447
     *
448
     * @param string $contents       = null
449
     * @param array  $params         = []
450
     * @param bool   $verifyChecksum = true
451
     *
452
     * @return array
453
     */
454
    protected function convertUploadParamsToHeaders($contents = null, array $params = [], $verifyChecksum = true)
455
    {
456
        $headers = [];
457
458
        if ($verifyChecksum) {
459
            $headers['ETag'] = md5($contents);
460
        }
461
462
        $availableParams = [
463
            'contentType' => 'Content-Type',
464
            'contentDisposition' => 'Content-Disposition',
465
            'deleteAfter' => 'X-Delete-After',
466
            'deleteAt' => 'X-Delete-At',
467
        ];
468
469
        foreach ($availableParams as $key => $header) {
470
            if (isset($params[$key])) {
471
                $headers[$header] = $params[$key];
472
            }
473
        }
474
475
        return $headers;
476
    }
477
478
    /**
479
     * Deletes container. Container must be empty in order to perform this operation.
480
     *
481
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
482
     */
483
    public function delete()
484
    {
485
        $response = $this->api->request('DELETE', $this->absolutePath());
486
487
        switch ($response->getStatusCode()) {
488
            case 204:
489
                // Container removed.
490
                return;
491
            case 404:
492
                throw new ApiRequestFailedException('Container "'.$this->name().'" was not found.');
493
            case 409:
494
                throw new ApiRequestFailedException('Container must be empty.');
495
        }
496
    }
497
}
498