Completed
Push — master ( f421ef...8ae150 )
by Timur
02:26
created

Container::deleteDir()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
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->absolutePath());
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
     * Absolute path to file from storage root.
105
     *
106
     * @param string $path = '' Relative file path.
107
     *
108
     * @return string
109
     */
110
    protected function absolutePath($path = '')
111
    {
112
        return '/'.$this->name().($path ? '/'.ltrim($path, '/') : '');
113
    }
114
115
    /**
116
     * Container name.
117
     *
118
     * @return string
119
     */
120
    public function name()
121
    {
122
        return $this->name;
123
    }
124
125
    /**
126
     * Container visibility type.
127
     *
128
     * @return string
129
     */
130
    public function type()
131
    {
132
        return $this->containerData('type', 'public');
133
    }
134
135
    /**
136
     * Container files count.
137
     *
138
     * @return int
139
     */
140
    public function filesCount()
141
    {
142
        return intval($this->containerData('count', 0));
143
    }
144
145
    /**
146
     * Container files count.
147
     *
148
     * @return int
149
     */
150
    public function count()
151
    {
152
        return $this->filesCount();
153
    }
154
155
    /**
156
     * Container size in bytes.
157
     *
158
     * @return int
159
     */
160
    public function size()
161
    {
162
        return intval($this->containerData('bytes', 0));
163
    }
164
165
    /**
166
     * Total uploaded (received) bytes.
167
     *
168
     * @return int
169
     */
170
    public function uploadedBytes()
171
    {
172
        return intval($this->containerData('rx_bytes', 0));
173
    }
174
175
    /**
176
     * Total downloaded (transmitted) bytes.
177
     *
178
     * @return int
179
     */
180
    public function downloadedBytes()
181
    {
182
        return intval($this->containerData('tx_bytes', 0));
183
    }
184
185
    /**
186
     * Returns JSON representation of container.
187
     *
188
     * @return array
189
     */
190
    public function jsonSerialize()
191
    {
192
        return [
193
            'name' => $this->name(),
194
            'type' => $this->type(),
195
            'files_count' => $this->filesCount(),
196
            'size' => $this->size(),
197
            'uploaded_bytes' => $this->uploadedBytes(),
198
            'downloaded_bytes' => $this->downloadedBytes(),
199
        ];
200
    }
201
202
    /**
203
     * Determines if container is public.
204
     *
205
     * @return bool
206
     */
207
    public function isPublic()
208
    {
209
        return $this->type() == 'public';
210
    }
211
212
    /**
213
     * Determines if container is private.
214
     *
215
     * @return bool
216
     */
217
    public function isPrivate()
218
    {
219
        return !$this->isPublic();
220
    }
221
222
    /**
223
     * Retrieves files from current container.
224
     *
225
     * @param string $directory        = null
226
     * @param string $prefixOrFullPath = null
227
     * @param string $delimiter        = null
228
     * @param int    $limit            = 10000
229
     * @param string $marker           = ''
230
     *
231
     * @return \ArgentCrusade\Selectel\CloudStorage\Contracts\Collections\CollectionContract
232
     */
233
    public function files($directory = null, $prefixOrFullPath = null, $delimiter = null, $limit = 10000, $marker = '')
234
    {
235
        $response = $this->api->request('GET', $this->absolutePath(), [
236
            'query' => [
237
                'limit' => intval($limit),
238
                'marker' => $marker,
239
                'path' => !is_null($directory) ? ltrim($directory, '/') : '',
240
                'prefix' => !is_null($prefixOrFullPath) ? ltrim($prefixOrFullPath, '/') : '',
241
                'delimiter' => !is_null($delimiter) ? $delimiter : '',
242
            ],
243
        ]);
244
245
        if ($response->getStatusCode() !== 200) {
246
            throw new ApiRequestFailedException('Unable to list container files.', $response->getStatusCode());
247
        }
248
249
        return new Collection(json_decode($response->getBody(), true));
250
    }
251
252
    /**
253
     * Retrieves file object container. This method does not actually download file, see File::download.
254
     *
255
     * @param string $path
256
     *
257
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\FileNotFoundException
258
     * @throws \LogicException
259
     *
260
     * @return \ArgentCrusade\Selectel\CloudStorage\Contracts\FileContract
261
     */
262
    public function getFile($path)
263
    {
264
        $files = $this->files(null, $path);
265
266
        if (!count($files)) {
267
            throw new FileNotFoundException('File "'.$path.'" was not found in container "'.$this->name().'".');
268
        }
269
270
        if (count($files) > 1) {
271
            throw new LogicException('There is more than one file that satisfies given path "'.$path.'".');
272
        }
273
274
        return new File($this->api, $this->name(), $files->get(0));
275
    }
276
277
    /**
278
     * Creates new directory.
279
     *
280
     * @param string $name Directory name.
281
     *
282
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
283
     *
284
     * @return string
285
     */
286
    public function createDir($name)
287
    {
288
        $response = $this->api->request('PUT', $this->absolutePath($name), [
289
            'headers' => [
290
                'Content-Type' => 'application/directory',
291
            ],
292
        ]);
293
294
        if ($response->getStatusCode() !== 201) {
295
            throw new ApiRequestFailedException('Unable to create directory "'.$name.'".', $response->getStatusCode());
296
        }
297
298
        return $response->getHeaderLine('ETag');
299
    }
300
301
    /**
302
     * Deletes directory.
303
     *
304
     * @param string $name Directory name.
305
     */
306
    public function deleteDir($name)
307
    {
308
        $response = $this->api->request('DELETE', $this->absolutePath($name));
309
310
        if ($response->getStatusCode() !== 204) {
311
            throw new ApiRequestFailedException('Unable to delete directory "'.$name.'".', $response->getStatusCode());
312
        }
313
314
        return true;
315
    }
316
317
    /**
318
     * Uploads file contents from string. Returns ETag header value if upload was successful.
319
     *
320
     * @param string $path           Remote path.
321
     * @param string $contents       File contents.
322
     * @param array  $params         = [] Upload params.
323
     * @param bool   $verifyChecksum = true
324
     *
325
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException
326
     *
327
     * @return string
328
     */
329
    public function uploadFromString($path, $contents, array $params = [], $verifyChecksum = true)
330
    {
331
        return $this->uploadFrom($path, $contents, $params, $verifyChecksum);
332
    }
333
334
    /**
335
     * Uploads file from stream. Returns ETag header value if upload was successful.
336
     *
337
     * @param string   $path     Remote path.
338
     * @param resource $resource Stream resource.
339
     * @param array    $params   = [] Upload params.
340
     *
341
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException
342
     *
343
     * @return string
344
     */
345
    public function uploadFromStream($path, $resource, array $params = [])
346
    {
347
        return $this->uploadFrom($path, $resource, $params, false);
348
    }
349
350
    /**
351
     * Upload file from string or stream resource.
352
     *
353
     * @param string            $path           Remote path.
354
     * @param string | resource $contents       File contents.
355
     * @param array             $params         = [] Upload params.
356
     * @param bool              $verifyChecksum = true
357
     *
358
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\UploadFailedException
359
     *
360
     * @return string
361
     */
362
    protected function uploadFrom($path, $contents, array $params = [], $verifyChecksum = true)
363
    {
364
        $response = $this->api->request('PUT', $this->absolutePath($path), [
365
            'headers' => $this->convertUploadParamsToHeaders($contents, $params, $verifyChecksum),
366
            'body' => $contents,
367
        ]);
368
369
        if ($response->getStatusCode() !== 201) {
370
            throw new UploadFailedException('Unable to upload file.', $response->getStatusCode());
371
        }
372
373
        return $response->getHeaderLine('ETag');
374
    }
375
376
    /**
377
     * Parses upload parameters and assigns them to appropriate HTTP headers.
378
     *
379
     * @param string $contents       = null
380
     * @param array  $params         = []
381
     * @param bool   $verifyChecksum = true
382
     *
383
     * @return array
384
     */
385
    protected function convertUploadParamsToHeaders($contents = null, array $params = [], $verifyChecksum = true)
386
    {
387
        $headers = [];
388
389
        if ($verifyChecksum) {
390
            $headers['ETag'] = md5($contents);
391
        }
392
393
        $availableParams = [
394
            'contentType' => 'Content-Type',
395
            'contentDisposition' => 'Content-Disposition',
396
            'deleteAfter' => 'X-Delete-After',
397
            'deleteAt' => 'X-Delete-At',
398
        ];
399
400
        foreach ($availableParams as $key => $header) {
401
            if (isset($params[$key])) {
402
                $headers[$header] = $params[$key];
403
            }
404
        }
405
406
        return $headers;
407
    }
408
409
    /**
410
     * Deletes container. Container must be empty in order to perform this operation.
411
     *
412
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
413
     */
414
    public function delete()
415
    {
416
        $response = $this->api->request('DELETE', $this->absolutePath());
417
418
        switch ($response->getStatusCode()) {
419
            case 204:
420
                // Container removed.
421
                return;
422
            case 404:
423
                throw new ApiRequestFailedException('Container "'.$this->name().'" was not found.');
424
            case 409:
425
                throw new ApiRequestFailedException('Container must be empty.');
426
        }
427
    }
428
}
429