File::read()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 0
1
<?php
2
3
namespace ArgentCrusade\Selectel\CloudStorage;
4
5
use LogicException;
6
use JsonSerializable;
7
use InvalidArgumentException;
8
use GuzzleHttp\Psr7\StreamWrapper;
9
use ArgentCrusade\Selectel\CloudStorage\Contracts\FileContract;
10
use ArgentCrusade\Selectel\CloudStorage\Contracts\Api\ApiClientContract;
11
use ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException;
12
13
class File implements FileContract, JsonSerializable
14
{
15
    /**
16
     * @var \ArgentCrusade\Selectel\CloudStorage\Contracts\Api\ApiClientContract
17
     */
18
    protected $api;
19
20
    /**
21
     * Container name.
22
     *
23
     * @var string
24
     */
25
    protected $container;
26
27
    /**
28
     * File info.
29
     *
30
     * @var array
31
     */
32
    protected $data;
33
34
    /**
35
     * Determines if current file was recently deleted.
36
     *
37
     * @var bool
38
     */
39
    protected $deleted = false;
40
41
    /**
42
     * @param \ArgentCrusade\Selectel\CloudStorage\Contracts\Api\ApiClientContract $api
43
     * @param string                                                               $container
44
     * @param array                                                                $data
45
     */
46
    public function __construct(ApiClientContract $api, $container, array $data)
47
    {
48
        $this->api = $api;
49
        $this->container = $container;
50
        $this->data = $data;
51
    }
52
53
    /**
54
     * Returns specific file data.
55
     *
56
     * @param string $key
57
     * @param mixed  $default = null
58
     *
59
     * @throws \LogicException
60
     *
61
     * @return mixed|null
62
     */
63
    protected function fileData($key, $default = null)
64
    {
65
        $this->guardDeletedFile();
66
67
        return isset($this->data[$key]) ? $this->data[$key] : $default;
68
    }
69
70
    /**
71
     * Absolute path to file from storage root.
72
     *
73
     * @param string $path = '' Relative file path.
74
     *
75
     * @return string
76
     */
77
    protected function absolutePath($path = '')
78
    {
79
        if (!$path) {
80
            $path = $this->path();
81
        }
82
83
        return '/'.$this->container().($path ? '/'.ltrim($path, '/') : '');
84
    }
85
86
    /**
87
     * Container name.
88
     *
89
     * @throws \LogicException
90
     *
91
     * @return string
92
     */
93
    public function container()
94
    {
95
        return $this->container;
96
    }
97
98
    /**
99
     * Full path to file.
100
     *
101
     * @throws \LogicException
102
     *
103
     * @return string
104
     */
105
    public function path()
106
    {
107
        return $this->fileData('name');
108
    }
109
110
    /**
111
     * File directory.
112
     *
113
     * @throws \LogicException
114
     *
115
     * @return string
116
     */
117
    public function directory()
118
    {
119
        $path = explode('/', $this->path());
120
121
        array_pop($path);
122
123
        return implode('/', $path);
124
    }
125
126
    /**
127
     * File name.
128
     *
129
     * @throws \LogicException
130
     *
131
     * @return string
132
     */
133
    public function name()
134
    {
135
        $path = explode('/', $this->path());
136
137
        return array_pop($path);
138
    }
139
140
    /**
141
     * File size in bytes.
142
     *
143
     * @throws \LogicException
144
     *
145
     * @return int
146
     */
147
    public function size()
148
    {
149
        return intval($this->fileData('bytes'));
150
    }
151
152
    /**
153
     * File content type.
154
     *
155
     * @throws \LogicException
156
     *
157
     * @return string
158
     */
159
    public function contentType()
160
    {
161
        return $this->fileData('content_type');
162
    }
163
164
    /**
165
     * Date of last modification.
166
     *
167
     * @throws \LogicException
168
     *
169
     * @return string
170
     */
171
    public function lastModifiedAt()
172
    {
173
        return $this->fileData('last_modified');
174
    }
175
176
    /**
177
     * File ETag.
178
     *
179
     * @throws \LogicException
180
     *
181
     * @return string
182
     */
183
    public function etag()
184
    {
185
        return $this->fileData('hash');
186
    }
187
188
    /**
189
     * Determines if current file was recently deleted.
190
     *
191
     * @return bool
192
     */
193
    public function isDeleted()
194
    {
195
        return $this->deleted === true;
196
    }
197
198
    /**
199
     * Reads file contents.
200
     *
201
     * @return string
202
     */
203
    public function read()
204
    {
205
        $response = $this->api->request('GET', $this->absolutePath());
206
207
        return (string) $response->getBody();
208
    }
209
210
    /**
211
     * Reads file contents as stream.
212
     *
213
     * @param bool $psr7Stream = false
214
     *
215
     * @return resource|\Psr\Http\Message\StreamInterface
216
     */
217
    public function readStream($psr7Stream = false)
218
    {
219
        $response = $this->api->request('GET', $this->absolutePath());
220
221
        if ($psr7Stream) {
222
            return $response->getBody();
223
        }
224
225
        return StreamWrapper::getResource($response->getBody());
226
    }
227
228
    /**
229
     * Rename file. New file name must be provided without path.
230
     *
231
     * @param string $name
232
     *
233
     * @throws \LogicException
234
     * @throws \InvalidArgumentException
235
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
236
     *
237
     * @return string
238
     */
239
    public function rename($name)
240
    {
241
        $this->guardDeletedFile();
242
243
        // If there is any slash character in new name, Selectel
244
        // will create new virtual directory and copy file to
245
        // this new one. Such behaviour may be unexpected.
246
247
        if (count(explode('/', $name)) > 1) {
248
            throw new InvalidArgumentException('File name can not contain "/" character.');
249
        }
250
251
        $destination = $this->directory().'/'.$name;
252
253
        $response = $this->api->request('PUT', $this->absolutePath($destination), [
254
            'headers' => [
255
                'X-Copy-From' => $this->absolutePath(),
256
                'Content-Length' => 0,
257
            ],
258
        ]);
259
260
        if ($response->getStatusCode() !== 201) {
261
            throw new ApiRequestFailedException(
262
                'Unable to rename file from "'.$this->name().'" to "'.$name.'" (path: "'.$this->directory().'").',
263
                $response->getStatusCode()
264
            );
265
        }
266
267
        // Since Selectel Storage does not provide such method as "rename",
268
        // we need to delete original file after copying. Also, "deleted"
269
        // flag needs to be reverted because file was actually renamed.
270
271
        $this->delete();
272
        $this->deleted = false;
273
274
        return $this->data['name'] = $destination;
275
    }
276
277
    /**
278
     * Copy file to given destination.
279
     *
280
     * @param string $destination
281
     * @param string $destinationContainer = null
282
     *
283
     * @throws \LogicException
284
     *
285
     * @return string
286
     */
287
    public function copy($destination, $destinationContainer = null)
288
    {
289
        $this->guardDeletedFile();
290
291
        if (is_null($destinationContainer)) {
292
            $destinationContainer = $this->container();
293
        }
294
295
        $fullDestination = '/'.$destinationContainer.'/'.ltrim($destination, '/');
296
297
        $response = $this->api->request('COPY', $this->absolutePath(), [
298
            'headers' => [
299
                'Destination' => $fullDestination,
300
            ],
301
        ]);
302
303
        if ($response->getStatusCode() !== 201) {
304
            throw new ApiRequestFailedException(
305
                'Unable to copy file from "'.$this->path().'" to "'.$destination.'".',
306
                $response->getStatusCode()
307
            );
308
        }
309
310
        return $fullDestination;
311
    }
312
313
    /**
314
     * Deletes file.
315
     *
316
     * @throws \LogicException
317
     * @throws \ArgentCrusade\Selectel\CloudStorage\Exceptions\ApiRequestFailedException
318
     */
319
    public function delete()
320
    {
321
        $this->guardDeletedFile();
322
323
        $response = $this->api->request('DELETE', $this->absolutePath());
324
325
        if ($response->getStatusCode() !== 204) {
326
            throw new ApiRequestFailedException('Unable to delete file "'.$this->path().'".', $response->getStatusCode());
327
        }
328
329
        // Set deleted flag to true, so any other calls to
330
        // this File will result in throwing exception.
331
332
        $this->deleted = true;
333
334
        return true;
335
    }
336
337
    /**
338
     * JSON representation of file.
339
     *
340
     * @return array
341
     */
342
    public function jsonSerialize()
343
    {
344
        return [
345
            'name' => $this->name(),
346
            'path' => $this->path(),
347
            'directory' => $this->directory(),
348
            'container' => $this->container(),
349
            'size' => $this->size(),
350
            'content_type' => $this->contentType(),
351
            'last_modified' => $this->lastModifiedAt(),
352
            'etag' => $this->etag(),
353
        ];
354
    }
355
356
    /**
357
     * Protects FileAPI from unwanted requests.
358
     *
359
     * @throws \LogicException
360
     */
361
    protected function guardDeletedFile()
362
    {
363
        if ($this->deleted === true) {
364
            throw new LogicException('File was deleted recently.');
365
        }
366
    }
367
}
368