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

File   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 486
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 35
lcom 1
cbo 13
dl 0
loc 486
ccs 0
cts 162
cp 0
rs 9.6
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A getHistory() 0 18 2
A postRestore() 0 6 1
B putChunk() 0 54 7
A put() 0 17 1
F _put() 0 94 24
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