Completed
Branch dev (d5d70c)
by Raffael
11:00
created

Files::putChunk()   D

Complexity

Conditions 12
Paths 109

Size

Total Lines 94
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 94
rs 4.8705
c 0
b 0
f 0
cc 12
eloc 63
nc 109
nop 10

How to fix   Long Method    Complexity    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\v2;
13
14
use Balloon\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\App\Api\v2\Exception.

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...
15
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
16
use Balloon\Filesystem\Node\Collection;
17
use Balloon\Server\AttributeDecorator as RoleAttributeDecorator;
18
use Micro\Http\Response;
19
use MongoDB\BSON\ObjectId;
20
21
class Files extends Nodes
22
{
23
    /**
24
     * @api {get} /api/v2/files/:id/history Get history
25
     * @apiVersion 2.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/v2/files/history?id=544627ed3c58891f058b4686&pretty"
34
     * curl -XGET "https://SERVER/api/v2/files/544627ed3c58891f058b4686/history?pretty"
35
     * curl -XGET "https://SERVER/api/v2/files/history?p=/absolute/path/to/my/file&pretty"
36
     *
37
     * @apiSuccess (200 OK) {object[]} - History
38
     * @apiSuccess (200 OK) {number} -.version Version
39
     * @apiSuccess (200 OK) {string} -.changed ISO806 timestamp
40
     * @apiSuccess (200 OK) {object} -.user User object
41
     * @apiSuccess (200 OK) {number} -.type Change type, there are five different change types including:</br>
42
     *  0 - Initially added</br>
43
     *  1 - Content modified</br>
44
     *  2 - Version rollback</br>
45
     *  3 - Deleted</br>
46
     *  4 - Undeleted
47
     * @apiSuccess (200 OK) {number} -.size Content size in bytes
48
     * @apiSuccessExample {json} Success-Response:
49
     * HTTP/1.1 200 OK
50
     * [
51
     *  {
52
     *      "version": 1,
53
     *      "changed": ""
54
     *      "user": {
55
     *          "id": "544627ed3c58891f058b4611",
56
     *          "name": "peter.meier"
57
     *      },
58
     *      "type": 0,
59
     *      "size": 178,
60
     *  }
61
     * ]
62
     *
63
     * @param RoleAttributeDecorator $role_decorator
64
     * @param string                 $id
65
     * @param string                 $p
66
     *
67
     * @return Response
68
     */
69
    public function getHistory(RoleAttributeDecorator $role_decorator, ?string $id = null, ?string $p = null): Response
70
    {
71
        $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...
72
        $body = [];
73
        foreach ($result as $version) {
74
            if ($version['user'] === null) {
75
                $user = null;
76
            } else {
77
                $user = $this->server->getUserById($version['user']);
78
                $user = $role_decorator->decorate($user, ['id', 'name', '_links']);
79
            }
80
81
            $body[] = [
82
                'version' => $version['version'],
83
                'changed' => $version['changed']->toDateTime()->format('c'),
84
                'type' => $version['type'],
85
                'size' => $version['size'],
86
                'user' => $user,
87
            ];
88
        }
89
90
        return (new Response())->setCode(200)->setBody($body);
91
    }
92
93
    /**
94
     * @api {post} /api/v2/files/:id/restore Rollback version
95
     * @apiVersion 2.0.0
96
     * @apiName postRestore
97
     * @apiGroup Node\File
98
     * @apiPermission none
99
     * @apiDescription Rollback to a recent version from history. Use the version number from history.
100
     * @apiUse _getNode
101
     *
102
     * @apiExample (cURL) example:
103
     * curl -XPOST "https://SERVER/api/v2/files/restore?id=544627ed3c58891f058b4686&pretty&vesion=11"
104
     * curl -XPOST "https://SERVER/api/v2/files/544627ed3c58891f058b4686/restore?pretty&version=1"
105
     * curl -XPOST "https://SERVER/api/v2/files/restore?p=/absolute/path/to/my/file&pretty&version=3"
106
     *
107
     * @apiParam (GET Parameter) {number} version The version from history to rollback to
108
     *
109
     * @apiSuccessExample {json} Success-Response:
110
     * HTTP/1.1 200 OK
111
     * {
112
     *      "id": "544627ed3c58891f058b4686"
113
     * }
114
     *
115
     * @param string $id
116
     * @param string $p
117
     * @param string $version
118
     *
119
     * @return Response
120
     */
121
    public function postRestore(int $version, ?string $id = null, ?string $p = null): Response
122
    {
123
        $node = $this->_getNode($id, $p);
124
        $node->restore($version);
125
        $result = $this->node_decorator->decorate($node);
126
127
        return (new Response())->setCode(200)->setBody($result);
128
    }
129
130
    /**
131
     * @api {put} /api/v2/files/chunk Upload file chunk
132
     * @apiVersion 2.0.0
133
     * @apiName putChunk
134
     * @apiGroup Node\File
135
     * @apiPermission none
136
     * @apiUse _getNode
137
     * @apuUse _conflictNode
138
     * @apiUse _writeAction
139
     * @apiDescription Upload a file chunk. Use this method if you have possible big files!
140
     * You have to manually splitt the binary data into
141
     * multiple chunks and upload them successively using this method. Once uploading the last chunk,
142
     * the server will automatically create or update the file node.
143
     * You may set the parent collection, name and or custom attributes only with the last request to save traffic.
144
     *
145
     * @apiExample (cURL) example:
146
     * # Upload a new file myfile.jpg into the collection 544627ed3c58891f058b4686.
147
     * 1. First splitt the file into multiple 8M (For example, you could also use a smaller or bigger size) chunks
148
     * 2. Create a unique name for the session (Could also be the filename), best thing is to create a UUIDv4
149
     * 3. Upload each chunk successively (follow the binary order of your file!) using the chunk PUT method
150
     *   (The server identifies each chunk with the index parameter, beginning with #1).
151
     * 4. If chunk number 3 will be reached, the server automatically place all chunks to the new file node
152
     *
153
     * curl -XPUT "https://SERVER/api/v2/files/chunk?collection=544627ed3c58891f058b4686&name=myfile.jpg&index=1&chunks=3&session=myuniquesession&size=12342442&pretty" --data-binary @chunk1.bin
154
     * curl -XPUT "https://SERVER/api/v2/files/chunk?collection=544627ed3c58891f058b4686&name=myfile.jpg&index=2&chunks=3&session=myuniquesession&size=12342442&pretty" --data-binary @chunk2.bin
155
     * curl -XPUT "https://SERVER/api/v2/files/chunk?collection=544627ed3c58891f058b4686&name=myfile.jpg&index=3&chunks=3&session=myuniquesession&size=12342442&pretty" --data-binary @chunk3.bin
156
     *
157
     * @apiParam (GET Parameter) {string} [id] Either id, p (path) of a file node or a parent collection id must be given
158
     * @apiParam (GET Parameter) {string} [p] Either id, p (path) of a file node or a parent collection id must be given
159
     * @apiParam (GET Parameter) {string} [collection] Either id, p (path) of a file node or a parent collection id must be given
160
     * (If none of them are given, the file will be placed to the root)
161
     * @apiParam (GET Parameter) {string} [name] Needs to be set if the chunk belongs to a new file
162
     * @apiParam (GET Parameter) {number} index Chunk ID (consider chunk order!)
163
     * @apiParam (GET Parameter) {number} chunks Total number of chunks
164
     * @apiParam (GET Parameter) {string} session A unique name which identifes a group of chunks (One file)
165
     * @apiParam (GET Parameter) {number} size The total file size in bytes
166
     * @apiParam (GET Parameter) {object} [attributes] Overwrite some attributes which are usually generated on the server
167
     * @apiParam (GET Parameter) {number} [attributes.created] Set specific created timestamp (UNIX timestamp format)
168
     * @apiParam (GET Parameter) {number} [attributes.changed] Set specific changed timestamp (UNIX timestamp format)
169
     *
170
     *
171
     * @apiSuccess (200 OK) {number} status Status Code
172
     * @apiSuccess (200 OK) {number} data Increased version number if the last chunk was uploaded and existing node was updated.
173
     * It will return the old version if the submited file content was equal to the existing one.
174
     *
175
     * @apiSuccess (201 Created) {number} status Status Code
176
     * @apiSuccess (201 Created) {string} data Node ID if the last chunk was uploaded and a new node was added
177
     *
178
     * @apiSuccess (206 Partial Content) {number} status Status Code
179
     * @apiSuccess (206 Partial Content) {string} data Chunk ID if it was not the last chunk
180
     * @apiSuccessExample {json} Success-Response (Not the last chunk yet):
181
     * HTTP/1.1 206 Partial Content
182
     * {
183
     *      "session": "78297329329389e332234341",
184
     *      "size": 12323244224,
185
     *      "chunks_left": 4
186
     * }
187
     *
188
     * @apiSuccessExample {json} Success-Response (New file created, Last chunk):
189
     * HTTP/1.1 201 Created
190
     * {
191
     *      "id": "78297329329389e332234342",
192
     *      "version": 1
193
     * }
194
     *
195
     * @apiSuccessExample {json} Success-Response (File updated, Last chunk):
196
     * HTTP/1.1 200 OK
197
     * {
198
     *      "id": "78297329329389e332234342",
199
     *      "version": 2
200
     * }
201
     *
202
     * @apiErrorExample {json} Error-Response (quota full):
203
     * HTTP/1.1 507 Insufficient Storage
204
     * {
205
     *      "status": 507
206
     *      "data": {
207
     *          "error": "Balloon\Exception\InsufficientStorage",
208
     *          "message": "user quota is full",
209
     *          "code": 66
210
     *      }
211
     * }
212
     *
213
     * @apiErrorExample {json} Error-Response (Size limit exceeded):
214
     * HTTP/1.1 400 Bad Request
215
     * {
216
     *      "status": 400,
217
     *      "data": {
218
     *          "error": "Balloon\\Exception\\Conflict",
219
     *          "message": "file size exceeded limit",
220
     *          "code": 17
221
     *      }
222
     * }
223
     *
224
     * @apiErrorExample {json} Error-Response (Chunks lost):
225
     * HTTP/1.1 400 Bad Request
226
     * {
227
     *      "status": 400,
228
     *      "data": {
229
     *          "error": "Balloon\\Exception\\Conflict",
230
     *          "message": "chunks lost, reupload all chunks",
231
     *          "code": 275
232
     *      }
233
     * }
234
     *
235
     * @apiErrorExample {json} Error-Response (Chunks invalid size):
236
     * HTTP/1.1 400 Bad Request
237
     * {
238
     *      "status": 400,
239
     *      "data": {
240
     *          "error": "Balloon\\Exception\\Conflict",
241
     *          "message": "merged chunks temp file size is not as expected",
242
     *          "code": 276
243
     *      }
244
     * }
245
     *
246
     * @param string $id
247
     * @param string $p
248
     * @param string $collection
249
     * @param string $name
250
     * @param int    $index
251
     * @param int    $chunks
252
     * @param string $session
253
     * @param int    $size
254
     * @param array  $attributes
255
     * @param int    $conflict
256
     *
257
     * @return Response
258
     */
259
    public function putChunk(
260
        ?ObjectId $session = null,
261
        ?string $id = null,
262
        ?string $p = null,
263
        ?string $collection = null,
264
        ?string $name = null,
265
        int $index = 1,
266
        int $chunks = 0,
267
        int $size = 0,
268
        array $attributes = [],
269
        int $conflict = 0
270
    ) {
271
        ini_set('auto_detect_line_endings', '1');
272
        $input_handler = fopen('php://input', 'rb');
273
        if ($index > $chunks) {
274
            throw new Exception\InvalidArgument('chunk index can not be greater than the total number of chunks');
275
        }
276
277
        if ($session === null) {
278
            $session = new ObjectId();
279
        }
280
281
        $folder = $this->server->getTempDir().DIRECTORY_SEPARATOR.'upload'.DIRECTORY_SEPARATOR.$this->user->getId();
282
283
        if (!file_exists($folder)) {
284
            mkdir($folder, 0700, true);
285
        }
286
287
        $file = $folder.DIRECTORY_SEPARATOR.$session;
288
289
        $tmp_size = 0;
290
        if (file_exists($file)) {
291
            $tmp_size = filesize($file);
292
        } elseif ($index > 1) {
293
            throw new Exception\Conflict(
294
                'chunks lost, reupload all chunks',
295
                Exception\Conflict::CHUNKS_LOST
296
            );
297
        }
298
299
        $session_handler = fopen($file, 'a+');
300
        while (!feof($input_handler)) {
301
            $data = fread($input_handler, 1024);
302
            $wrote = fwrite($session_handler, $data);
303
            $tmp_size += $wrote;
304
305
            if ($tmp_size > (int) $this->server->getMaxFileSize()) {
306
                fclose($input_handler);
307
                fclose($session_handler);
308
                unlink($file);
309
310
                throw new Exception\InsufficientStorage(
311
                    'file size exceeded limit',
312
                    Exception\InsufficientStorage::FILE_SIZE_LIMIT
313
                );
314
            }
315
        }
316
317
        if ($index === $chunks) {
318
            clearstatcache();
319
            if (!is_readable($file)) {
320
                throw new Exception\Conflict(
321
                    'chunks lost, reupload all chunks',
322
                    Exception\Conflict::CHUNKS_LOST
323
                );
324
            }
325
326
            if ($tmp_size !== $size) {
327
                fclose($session_handler);
328
                unlink($file);
329
330
                throw new Exception\Conflict(
331
                    'merged chunks temp file size is not as expected',
332
                    Exception\Conflict::CHUNKS_INVALID_SIZE
333
                );
334
            }
335
336
            try {
337
                $attributes = $this->_verifyAttributes($attributes);
338
339
                return $this->_put($file, $id, $p, $collection, $name, $attributes, $conflict);
340
            } catch (\Exception $e) {
341
                unlink($file);
342
343
                throw $e;
344
            }
345
        } else {
346
            return (new Response())->setCode(206)->setBody([
347
                'session' => (string) $session,
348
                'size' => $tmp_size,
349
                'chunks_left' => $chunks - $index,
350
            ]);
351
        }
352
    }
353
354
    /**
355
     * @api {put} /api/v2/files Upload file
356
     * @apiVersion 2.0.0
357
     * @apiName put
358
     * @apiGroup Node\File
359
     * @apiPermission none
360
     * @apiUse _getNode
361
     * @apiUse _conflictNode
362
     * @apiUse _writeAction
363
     *
364
     * @apiDescription Upload an entire file in one-shot. Attention, there is file size limit,
365
     * if you have possible big files use the method PUT chunk!
366
     *
367
     * @apiExample (cURL) example:
368
     * #Update content of file 544627ed3c58891f058b4686
369
     * curl -XPUT "https://SERVER/api/v2/files?id=544627ed3c58891f058b4686" --data-binary myfile.txt
370
     * curl -XPUT "https://SERVER/api/v2/files/544627ed3c58891f058b4686" --data-binary myfile.txt
371
     *
372
     * #Upload new file under collection 544627ed3c58891f058b3333
373
     * curl -XPUT "https://SERVER/api/v2/files?collection=544627ed3c58891f058b3333&name=myfile.txt" --data-binary myfile.txt
374
     *
375
     * @apiParam (GET Parameter) {string} [id] Either id, p (path) of a file node or a parent collection id must be given
376
     *
377
     * @apiParam (GET Parameter) {string} [id] Either id, p (path) of a file node or a parent collection id must be given
378
     * @apiParam (GET Parameter) {string} [p] Either id, p (path) of a file node or a parent collection id must be given
379
     * @apiParam (GET Parameter) {string} [collection] Either id, p (path) of a file node or a parent collection id must be given
380
     * (If none of them are given, the file will be placed to the root)
381
     * @apiParam (GET Parameter) {string} [name] Needs to be set if the chunk belongs to a new file
382
     * or to identify an existing child file if a collection id was set
383
     * @apiParam (GET Parameter) {object} attributes Overwrite some attributes which are usually generated on the server
384
     * @apiParam (GET Parameter) {number} attributes.created Set specific created timestamp (UNIX timestamp format)
385
     * @apiParam (GET Parameter) {number} attributes.changed Set specific changed timestamp (UNIX timestamp format)
386
     *
387
     * @apiSuccess (200 OK) {number} status Status Code
388
     * @apiSuccess (200 OK) {number} data Increased version number if an existing file was updated. It will return
389
     * the old version if the submited file content was equal to the existing one.
390
     *
391
     * @apiSuccess (201 Created) {number} status Status Code
392
     * @apiSuccess (201 Created) {string} data Node ID
393
     * @apiSuccessExample {json} Success-Response (New file created):
394
     * HTTP/1.1 201 Created
395
     * {
396
     *      "status": 201,
397
     *      "data": "78297329329389e332234342"
398
     * }
399
     *
400
     * @apiSuccessExample {json} Success-Response (File updated):
401
     * HTTP/1.1 200 OK
402
     * {
403
     *      "status": 200,
404
     *      "data": 2
405
     * }
406
     *
407
     * @apiErrorExample {json} Error-Response (quota full):
408
     * HTTP/1.1 507 Insufficient Storage
409
     * {
410
     *      "status": 507
411
     *      "data": {
412
     *          "error": "Balloon\Exception\InsufficientStorage",
413
     *          "message": "user quota is full",
414
     *          "code": 65
415
     *      }
416
     * }
417
     *
418
     * @apiErrorExample {json} Error-Response (Size limit exceeded):
419
     * HTTP/1.1 400 Bad Request
420
     * {
421
     *      "status": 400,
422
     *      "data": {
423
     *          "error": "Balloon\\Exception\\Conflict",
424
     *          "message": "file size exceeded limit",
425
     *          "code": 17
426
     *      }
427
     * }
428
     *
429
     * @param string $id
430
     * @param string $p
431
     * @param string $collection
432
     * @param string $name
433
     * @param array  $attributes
434
     * @param int    $conflict
435
     *
436
     * @return Response
437
     */
438
    public function put(
439
        ?string $id = null,
440
        ?string $p = null,
441
        ?string $collection = null,
442
        ?string $name = null,
443
        array $attributes = [],
444
        int $conflict = 0
445
    ): Response {
446
        $attributes = $this->_verifyAttributes($attributes);
447
448
        ini_set('auto_detect_line_endings', '1');
449
        $content = fopen('php://input', 'rb');
450
451
        return $this->_put($content, $id, $p, $collection, $name, $attributes, $conflict);
452
    }
453
454
    /**
455
     * Add or update file.
456
     *
457
     * @param resource|string $content
458
     * @param string          $id
459
     * @param string          $p
460
     * @param string          $collection
461
     * @param string          $name
462
     * @param array           $attributes
463
     * @param int             $conflict
464
     *
465
     * @return Response
466
     */
467
    protected function _put(
468
        $content,
469
        ?string $id = null,
470
        ?string $p = null,
471
        ?string $collection = null,
472
        ?string $name = null,
473
        array $attributes = [],
474
        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...
475
    ): Response {
476
        if (null === $id && null === $p && null === $name) {
477
            throw new Exception\InvalidArgument('neither id, p nor name was set');
478
        }
479
480
        if (null !== $p && null !== $name) {
481
            throw new Exception\InvalidArgument('p and name can not be used at the same time');
482
        }
483
484
        try {
485
            if (null !== $p) {
486
                $node = $this->_getNode(null, $p);
487
                $result = $node->put($content, false, $attributes);
488
                $result = $this->node_decorator->decorate($node);
489
490
                return (new Response())->setCode(200)->setBody($result);
491
            }
492
            if (null !== $id && null === $collection) {
493
                $node = $this->_getNode($id);
494
                $result = $node->put($content, false, $attributes);
495
                $result = $this->node_decorator->decorate($node);
496
497
                return (new Response())->setCode(200)->setBody($result);
498
            }
499
            if (null === $p && null === $id && null !== $name) {
500
                $collection = $this->_getNode($collection, null, Collection::class, false, true);
501
502
                if ($collection->childExists($name)) {
503
                    $child = $collection->getChild($name);
504
                    $result = $child->put($content, false, $attributes);
505
                    $result = $this->node_decorator->decorate($child);
506
507
                    return (new Response())->setCode(200)->setBody($result);
508
                }
509
                if (!is_string($name) || empty($name)) {
510
                    throw new Exception\InvalidArgument('name must be a valid string');
511
                }
512
513
                $result = $collection->addFile($name, $content, $attributes);
514
                $result = $this->node_decorator->decorate($result);
515
516
                return (new Response())->setCode(201)->setBody($result);
517
            }
518
        } catch (ForbiddenException $e) {
519
            throw new Exception\Conflict(
520
                'a node called '.$name.' does already exists in this collection',
521
                Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
522
            );
523
        } catch (Exception\NotFound $e) {
524
            if (null !== $p && null === $id) {
525
                if (!is_string($p) || empty($p)) {
526
                    throw new Exception\InvalidArgument('path (p) must be a valid string');
527
                }
528
529
                $parent_path = dirname($p);
530
                $name = basename($p);
531
532
                try {
533
                    $parent = $this->fs->findNodeByPath($parent_path, Collection::class);
534
535
                    if (!is_string($name) || empty($name)) {
536
                        throw new Exception\InvalidArgument('name must be a valid string');
537
                    }
538
539
                    $result = $parent->addFile($name, $content, $attributes);
540
                    $result = $this->node_decorator->decorate($result);
541
542
                    return (new Response())->setCode(201)->setBody($result);
543
                } catch (Exception\NotFound $e) {
544
                    throw new Exception('parent collection '.$parent_path.' was not found');
545
                }
546
            } else {
547
                throw $e;
548
            }
549
        }
550
    }
551
}
552