Completed
Pull Request — master (#141)
by Raffael
11:16
created

File::putChunk()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 54
ccs 0
cts 47
cp 0
rs 8.0703
c 0
b 0
f 0
cc 7
nc 7
nop 10
crap 56

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\App\Api\v1;
13
14
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
15
use Balloon\Filesystem\Exception;
16
use Balloon\Filesystem\Node\Collection;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\App\Api\v1\Collection.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
17
use Balloon\Helper;
18
use Micro\Http\Response;
19
use MongoDB\BSON\ObjectId;
20
21
class File extends Node
22
{
23
    /**
24
     * @api {get} /api/v1/file/history?id=:id Get history
25
     * @apiVersion 1.0.0
26
     * @apiName getHistory
27
     * @apiGroup Node\File
28
     * @apiPermission none
29
     * @apiDescription Get a full change history of a file
30
     * @apiUse _getNode
31
     *
32
     * @apiExample (cURL) example:
33
     * curl -XGET "https://SERVER/api/v1/file/history?id=544627ed3c58891f058b4686&pretty"
34
     * curl -XGET "https://SERVER/api/v1/file/544627ed3c58891f058b4686/history?pretty"
35
     * curl -XGET "https://SERVER/api/v1/file/history?p=/absolute/path/to/my/file&pretty"
36
     *
37
     * @apiSuccess (200 OK) {number} status Status Code
38
     * @apiSuccess (200 OK) {object[]} data History
39
     * @apiSuccess (200 OK) {number} data.version Version
40
     * @apiSuccess (200 OK) {object} data.changed Changed timestamp
41
     * @apiSuccess (200 OK) {number} data.changed.sec Changed timestamp in Unix time
42
     * @apiSuccess (200 OK) {number} data.changed.usec Additional microseconds to changed Unix timestamp
43
     * @apiSuccess (200 OK) {string} data.user User which changed the version
44
     * @apiSuccess (200 OK) {number} data.type Change type, there are five different change types including:</br>
45
     *  0 - Initially added</br>
46
     *  1 - Content modified</br>
47
     *  2 - Version rollback</br>
48
     *  3 - Deleted</br>
49
     *  4 - Undeleted
50
     * @apiSuccess (200 OK) {object} data.file Reference to the content
51
     * @apiSuccess (200 OK) {string} data.file.id Content reference ID
52
     * @apiSuccess (200 OK) {number} data.size Content size in bytes
53
     * @apiSuccess (200 OK) {string} data.mime Content mime type
54
     * @apiSuccessExample {json} Success-Response:
55
     * HTTP/1.1 200 OK
56
     * {
57
     *      "status": 200,
58
     *      "data": [
59
     *          {
60
     *              "version": 1,
61
     *              "changed": {
62
     *                  "sec": 1413883885,
63
     *                  "usec": 876000
64
     *              },
65
     *              "user": "peter.meier",
66
     *              "type": 0,
67
     *              "file": {
68
     *                  "$id": "544627ed3c58891f058b4688"
69
     *              },
70
     *              "size": 178,
71
     *              "mime": "text\/plain"
72
     *          }
73
     *      ]
74
     * }
75
     *
76
     * @param string $id
77
     * @param string $p
78
     */
79
    public function getHistory(?string $id = null, ?string $p = null): Response
80
    {
81
        $result = $this->_getNode($id, $p)->getHistory();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Balloon\Filesystem\Node\NodeInterface as the method getHistory() does only exist in the following implementations of said interface: Balloon\Filesystem\Node\File.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
82
        $body = [];
83
84
        foreach ($result as $version) {
85
            $v = (array) $version;
86
87
            $v['user'] = $this->server->getUserById($version['user'])->getUsername();
88
            $v['changed'] = Helper::DateTimeToUnix($version['changed']);
89
            $body[] = $v;
90
        }
91
92
        return (new Response())->setCode(200)->setBody([
93
            'status' => 200,
94
            'data' => $body,
95
        ]);
96
    }
97
98
    /**
99
     * @api {post} /api/v1/file/restore?id=:id Rollback version
100
     * @apiVersion 1.0.0
101
     * @apiName postRestore
102
     * @apiGroup Node\File
103
     * @apiPermission none
104
     * @apiDescription Rollback to a recent version from history. Use the version number from history.
105
     * @apiUse _getNode
106
     *
107
     * @apiExample (cURL) example:
108
     * curl -XPOST "https://SERVER/api/v1/file/restore?id=544627ed3c58891f058b4686&pretty&vesion=11"
109
     * curl -XPOST "https://SERVER/api/v1/file/544627ed3c58891f058b4686/restore?pretty&version=1"
110
     * curl -XPOST "https://SERVER/api/v1/file/restore?p=/absolute/path/to/my/file&pretty&version=3"
111
     *
112
     * @apiParam (GET Parameter) {number} version The version from history to rollback to
113
     *
114
     * @apiSuccessExample {json} Success-Response:
115
     * HTTP/1.1 204 No Content
116
     *
117
     * @param string $id
118
     * @param string $p
119
     * @param string $version
120
     */
121
    public function postRestore(int $version, ?string $id = null, ?string $p = null): Response
122
    {
123
        $result = $this->_getNode($id, $p)->restore($version);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Balloon\Filesystem\Node\NodeInterface as the method restore() does only exist in the following implementations of said interface: Balloon\Filesystem\Node\File.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
Unused Code introduced by
$result is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
124
125
        return (new Response())->setCode(204);
126
    }
127
128
    /**
129
     * @api {put} /api/v1/file/chunk Upload file chunk
130
     * @apiVersion 1.0.0
131
     * @apiName putChunk
132
     * @apiGroup Node\File
133
     * @apiPermission none
134
     * @apiUse _getNode
135
     * @apuUse _conflictNode
136
     * @apiUse _writeAction
137
     * @apiDescription Upload a file chunk. Use this method if you have possible big files!
138
     * You have to manually splitt the binary data into
139
     * multiple chunks and upload them successively using this method. Once uploading the last chunk,
140
     * the server will automatically create or update the file node.
141
     * You may set the parent collection, name and or custom attributes only with the last request to save traffic.
142
     *
143
     * @apiExample (cURL) example:
144
     * # Upload a new file myfile.jpg into the collection 544627ed3c58891f058b4686.
145
     * 1. First splitt the file into multiple 8M (For example, you could also use a smaller or bigger size) chunks
146
     * 2. Create a unique name for the chunkgroup (Could also be the filename), best thing is to create a UUIDv4
147
     * 3. Upload each chunk successively (follow the binary order of your file!) using the chunk PUT method
148
     *   (The server identifies each chunk with the index parameter, beginning with #1).
149
     * 4. If chunk number 3 will be reached, the server automatically place all chunks to the new file node
150
     *
151
     * curl -XPUT "https://SERVER/api/v1/file/chunk?collection=544627ed3c58891f058b4686&name=myfile.jpg&index=1&chunks=3&chunkgroup=myuniquechunkgroup&size=12342442&pretty" --data-binary @chunk1.bin
152
     * curl -XPUT "https://SERVER/api/v1/file/chunk?collection=544627ed3c58891f058b4686&name=myfile.jpg&index=2&chunks=3&chunkgroup=myuniquechunkgroup&size=12342442&pretty" --data-binary @chunk2.bin
153
     * curl -XPUT "https://SERVER/api/v1/file/chunk?collection=544627ed3c58891f058b4686&name=myfile.jpg&index=3&chunks=3&chunkgroup=myuniquechunkgroup&size=12342442&pretty" --data-binary @chunk3.bin
154
     *
155
     * @apiParam (GET Parameter) {string} [id] Either id, p (path) of a file node or a parent collection id must be given
156
     * @apiParam (GET Parameter) {string} [p] Either id, p (path) of a file node or a parent collection id must be given
157
     * @apiParam (GET Parameter) {string} [collection] Either id, p (path) of a file node or a parent collection id must be given
158
     * (If none of them are given, the file will be placed to the root)
159
     * @apiParam (GET Parameter) {string} [name] Needs to be set if the chunk belongs to a new file
160
     * @apiParam (GET Parameter) {number} index Chunk ID (consider chunk order!)
161
     * @apiParam (GET Parameter) {number} chunks Total number of chunks
162
     * @apiParam (GET Parameter) {string} chunkgroup A unique name which identifes a group of chunks (One file)
163
     * @apiParam (GET Parameter) {number} size The total file size in bytes
164
     * @apiParam (GET Parameter) {object} [attributes] Overwrite some attributes which are usually generated on the server
165
     * @apiParam (GET Parameter) {number} [attributes.created] Set specific created timestamp (UNIX timestamp format)
166
     * @apiParam (GET Parameter) {number} [attributes.changed] Set specific changed timestamp (UNIX timestamp format)
167
     *
168
     *
169
     * @apiSuccess (200 OK) {number} status Status Code
170
     * @apiSuccess (200 OK) {number} data Increased version number if the last chunk was uploaded and existing node was updated.
171
     * It will return the old version if the submited file content was equal to the existing one.
172
     *
173
     * @apiSuccess (201 Created) {number} status Status Code
174
     * @apiSuccess (201 Created) {string} data Node ID if the last chunk was uploaded and a new node was added
175
     *
176
     * @apiSuccess (206 Partial Content) {number} status Status Code
177
     * @apiSuccess (206 Partial Content) {string} data Chunk ID if it was not the last chunk
178
     * @apiSuccessExample {json} Success-Response (Not the last chunk yet):
179
     * HTTP/1.1 206 Partial Content
180
     * {
181
     *      "status": 206,
182
     *      "data": "1"
183
     * }
184
     *
185
     * @apiSuccessExample {json} Success-Response (New file created, Last chunk):
186
     * HTTP/1.1 201 Created
187
     * {
188
     *      "status": 201,
189
     *      "data": "78297329329389e332234342"
190
     * }
191
     *
192
     * @apiSuccessExample {json} Success-Response (File updated, Last chunk):
193
     * HTTP/1.1 200 OK
194
     * {
195
     *      "status": 200,
196
     *      "data": 2
197
     * }
198
     *
199
     * @apiErrorExample {json} Error-Response (quota full):
200
     * HTTP/1.1 507 Insufficient Storage
201
     * {
202
     *      "status": 507
203
     *      "data": {
204
     *          "error": "Balloon\Exception\InsufficientStorage",
205
     *          "message": "user quota is full",
206
     *          "code": 66
207
     *      }
208
     * }
209
     *
210
     * @apiErrorExample {json} Error-Response (Size limit exceeded):
211
     * HTTP/1.1 400 Bad Request
212
     * {
213
     *      "status": 400,
214
     *      "data": {
215
     *          "error": "Balloon\\Exception\\Conflict",
216
     *          "message": "file size exceeded limit",
217
     *          "code": 17
218
     *      }
219
     * }
220
     *
221
     * @apiErrorExample {json} Error-Response (Chunks lost):
222
     * HTTP/1.1 400 Bad Request
223
     * {
224
     *      "status": 400,
225
     *      "data": {
226
     *          "error": "Balloon\\Exception\\Conflict",
227
     *          "message": "chunks lost, reupload all chunks",
228
     *          "code": 275
229
     *      }
230
     * }
231
     *
232
     * @apiErrorExample {json} Error-Response (Chunks invalid size):
233
     * HTTP/1.1 400 Bad Request
234
     * {
235
     *      "status": 400,
236
     *      "data": {
237
     *          "error": "Balloon\\Exception\\Conflict",
238
     *          "message": "merged chunks temp file size is not as expected",
239
     *          "code": 276
240
     *      }
241
     * }
242
     *
243
     * @param string $id
244
     * @param string $p
245
     * @param string $collection
246
     * @param string $name
247
     *
248
     * @return Response
249
     */
250
    public function putChunk(
251
        string $chunkgroup,
252
        ?string $id = null,
253
        ?string $p = null,
254
        ?string $collection = null,
255
        ?string $name = null,
256
        int $index = 1,
257
        int $chunks = 0,
258
        int $size = 0,
0 ignored issues
show
Unused Code introduced by
The parameter $size is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
259
        array $attributes = [],
260
        int $conflict = 0
261
    ) {
262
        ini_set('auto_detect_line_endings', '1');
263
        $input = fopen('php://input', 'rb');
264
        if (!is_string($chunkgroup) || empty($chunkgroup)) {
265
            throw new Exception\InvalidArgument('chunkgroup must be valid unique string');
266
        }
267
268
        if ($index > $chunks) {
269
            throw new Exception\InvalidArgument('chunk index can not be greater than the total number of chunks');
270
        }
271
272
        if (!preg_match('#^([A-Za-z0-9\.\-_])+$#', $chunkgroup)) {
273
            throw new Exception\InvalidArgument('chunkgroup may only contain #^[(A-Za-z0-9\.\-_])+$#');
274
        }
275
276
        $session = $this->db->selectCollection('fs.files')->findOne([
277
            'metadata.chunkgroup' => $this->server->getIdentity()->getId().'_'.$chunkgroup,
278
        ]);
279
280
        if ($session === null) {
281
            $session = $this->storage->storeTemporaryFile($input, $this->server->getIdentity());
282
            $this->db->selectCollection('fs.files')->updateOne(
283
                ['_id' => $session],
284
                ['$set' => [
285
                    'metadata.chunkgroup' => $this->server->getIdentity()->getId().'_'.$chunkgroup,
286
                ]]
287
            );
288
        } else {
289
            $session = $session['_id'];
290
            $this->storage->storeTemporaryFile($input, $this->server->getIdentity(), $session);
291
        }
292
293
        if ($index === $chunks) {
294
            $attributes = $this->_verifyAttributes($attributes);
295
296
            return $this->_put($session, $id, $p, $collection, $name, $attributes, $conflict);
297
        }
298
299
        return (new Response())->setCode(206)->setBody([
300
                'status' => 206,
301
                'data' => $index,
302
            ]);
303
    }
304
305
    /**
306
     * @api {put} /api/v1/file Upload file
307
     * @apiVersion 1.0.0
308
     * @apiName put
309
     * @apiGroup Node\File
310
     * @apiPermission none
311
     * @apiUse _getNode
312
     * @apiUse _conflictNode
313
     * @apiUse _writeAction
314
     *
315
     * @apiDescription Upload an entire file in one-shot. Attention, there is file size limit,
316
     * if you have possible big files use the method PUT chunk!
317
     *
318
     * @apiExample (cURL) example:
319
     * #Update content of file 544627ed3c58891f058b4686
320
     * curl -XPUT "https://SERVER/api/v1/file?id=544627ed3c58891f058b4686" --data-binary myfile.txt
321
     * curl -XPUT "https://SERVER/api/v1/file/544627ed3c58891f058b4686" --data-binary myfile.txt
322
     *
323
     * #Upload new file under collection 544627ed3c58891f058b3333
324
     * curl -XPUT "https://SERVER/api/v1/file?collection=544627ed3c58891f058b3333&name=myfile.txt" --data-binary myfile.txt
325
     *
326
     * @apiParam (GET Parameter) {string} [id] Either id, p (path) of a file node or a parent collection id must be given
327
     *
328
     * @apiParam (GET Parameter) {string} [id] Either id, p (path) of a file node or a parent collection id must be given
329
     * @apiParam (GET Parameter) {string} [p] Either id, p (path) of a file node or a parent collection id must be given
330
     * @apiParam (GET Parameter) {string} [collection] Either id, p (path) of a file node or a parent collection id must be given
331
     * (If none of them are given, the file will be placed to the root)
332
     * @apiParam (GET Parameter) {string} [name] Needs to be set if the chunk belongs to a new file
333
     * or to identify an existing child file if a collection id was set
334
     * @apiParam (GET Parameter) {object} attributes Overwrite some attributes which are usually generated on the server
335
     * @apiParam (GET Parameter) {number} attributes.created Set specific created timestamp (UNIX timestamp format)
336
     * @apiParam (GET Parameter) {number} attributes.changed Set specific changed timestamp (UNIX timestamp format)
337
     *
338
     * @apiSuccess (200 OK) {number} status Status Code
339
     * @apiSuccess (200 OK) {number} data Increased version number if an existing file was updated. It will return
340
     * the old version if the submited file content was equal to the existing one.
341
     *
342
     * @apiSuccess (201 Created) {number} status Status Code
343
     * @apiSuccess (201 Created) {string} data Node ID
344
     * @apiSuccessExample {json} Success-Response (New file created):
345
     * HTTP/1.1 201 Created
346
     * {
347
     *      "status": 201,
348
     *      "data": "78297329329389e332234342"
349
     * }
350
     *
351
     * @apiSuccessExample {json} Success-Response (File updated):
352
     * HTTP/1.1 200 OK
353
     * {
354
     *      "status": 200,
355
     *      "data": 2
356
     * }
357
     *
358
     * @apiErrorExample {json} Error-Response (quota full):
359
     * HTTP/1.1 507 Insufficient Storage
360
     * {
361
     *      "status": 507
362
     *      "data": {
363
     *          "error": "Balloon\Exception\InsufficientStorage",
364
     *          "message": "user quota is full",
365
     *          "code": 65
366
     *      }
367
     * }
368
     *
369
     * @apiErrorExample {json} Error-Response (Size limit exceeded):
370
     * HTTP/1.1 400 Bad Request
371
     * {
372
     *      "status": 400,
373
     *      "data": {
374
     *          "error": "Balloon\\Exception\\Conflict",
375
     *          "message": "file size exceeded limit",
376
     *          "code": 17
377
     *      }
378
     * }
379
     *
380
     * @param string $id
381
     * @param string $p
382
     * @param string $collection
383
     * @param string $name
384
     */
385
    public function put(
386
        ?string $id = null,
387
        ?string $p = null,
388
        ?string $collection = null,
389
        ?string $name = null,
390
        array $attributes = [],
391
        int $conflict = 0
392
    ): Response {
393
        $attributes = $this->_verifyAttributes($attributes);
394
395
        ini_set('auto_detect_line_endings', '1');
396
        $input = fopen('php://input', 'rb');
397
398
        $session = $this->storage->storeTemporaryFile($input, $this->server->getIdentity());
399
400
        return $this->_put($session, $id, $p, $collection, $name, $attributes, $conflict);
401
    }
402
403
    /**
404
     * Add or update file.
405
     *
406
     * @param ObjecId $session
407
     * @param string  $id
408
     * @param string  $p
409
     * @param string  $collection
410
     * @param string  $name
411
     */
412
    protected function _put(
413
        ObjectId $session,
414
        ?string $id = null,
415
        ?string $p = null,
416
        ?string $collection = null,
417
        ?string $name = null,
418
        array $attributes = [],
419
        int $conflict = 0
0 ignored issues
show
Unused Code introduced by
The parameter $conflict is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
420
    ): Response {
421
        if (null === $id && null === $p && null === $name) {
422
            throw new Exception\InvalidArgument('neither id, p nor name was set');
423
        }
424
425
        if (null !== $p && null !== $name) {
426
            throw new Exception\InvalidArgument('p and name can not be used at the same time');
427
        }
428
429
        try {
430
            if (null !== $p) {
431
                $node = $this->_getNode(null, $p);
432
                $result = $node->setContent($session, $attributes);
433
434
                return (new Response())->setCode(200)->setBody([
435
                    'status' => 200,
436
                    'data' => $result,
437
                ]);
438
            }
439
            if (null !== $id && null === $collection) {
440
                $node = $this->_getNode($id);
441
                $result = $node->setContent($session, $attributes);
442
443
                return (new Response())->setCode(200)->setBody([
444
                    'status' => 200,
445
                    'data' => $result,
446
                ]);
447
            }
448
            if (null === $p && null === $id && null !== $name) {
449
                $collection = $this->_getNode($collection, null, Collection::class, false, true);
450
451
                if ($collection->childExists($name)) {
452
                    $child = $collection->getChild($name);
453
                    $result = $child->setContent($session, $attributes);
454
455
                    return (new Response())->setCode(200)->setBody([
456
                        'status' => 200,
457
                        'data' => $result,
458
                    ]);
459
                }
460
                if (!is_string($name) || empty($name)) {
461
                    throw new Exception\InvalidArgument('name must be a valid string');
462
                }
463
464
                $result = $collection->addFile($name, $session, $attributes)->getId();
465
466
                return (new Response())->setCode(201)->setBody([
467
                    'status' => 201,
468
                    'data' => (string) $result,
469
                ]);
470
            }
471
        } catch (ForbiddenException $e) {
472
            throw new Exception\Conflict(
473
                'a node called '.$name.' does already exists in this collection',
474
                Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
475
            );
476
        } catch (Exception\NotFound $e) {
477
            if (null !== $p && null === $id) {
478
                if (!is_string($p) || empty($p)) {
479
                    throw new Exception\InvalidArgument('path (p) must be a valid string');
480
                }
481
482
                $parent_path = dirname($p);
483
                $name = basename($p);
484
485
                try {
486
                    $parent = $this->fs->findNodeByPath($parent_path, Collection::class);
487
488
                    if (!is_string($name) || empty($name)) {
489
                        throw new Exception\InvalidArgument('name must be a valid string');
490
                    }
491
492
                    $result = $parent->addFile($name, $session, $attributes)->getId();
493
494
                    return (new Response())->setCode(201)->setBody([
495
                        'status' => 201,
496
                        'data' => (string) $result,
497
                    ]);
498
                } catch (Exception\NotFound $e) {
499
                    throw new Exception('parent collection '.$parent_path.' was not found');
500
                }
501
            } else {
502
                throw $e;
503
            }
504
        }
505
    }
506
}
507