Completed
Push — master ( 37faaa...541bbf )
by Raffael
10:18 queued 06:30
created

File::getStorage()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 0
cts 14
cp 0
rs 9.1111
c 0
b 0
f 0
cc 6
nc 4
nop 3
crap 42
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2019 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 Balloon\Session\SessionInterface;
19
use Micro\Http\Response;
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
     * @apiSuccess (200 OK) {number} status Status Code
169
     * @apiSuccess (200 OK) {number} data Increased version number if the last chunk was uploaded and existing node was updated.
170
     * It will return the old version if the submited file content was equal to the existing one.
171
     *
172
     * @apiSuccess (201 Created) {number} status Status Code
173
     * @apiSuccess (201 Created) {string} data Node ID if the last chunk was uploaded and a new node was added
174
     *
175
     * @apiSuccess (206 Partial Content) {number} status Status Code
176
     * @apiSuccess (206 Partial Content) {string} data Chunk ID if it was not the last chunk
177
     * @apiSuccessExample {json} Success-Response (Not the last chunk yet):
178
     * HTTP/1.1 206 Partial Content
179
     * {
180
     *      "status": 206,
181
     *      "data": "1"
182
     * }
183
     *
184
     * @apiSuccessExample {json} Success-Response (New file created, Last chunk):
185
     * HTTP/1.1 201 Created
186
     * {
187
     *      "status": 201,
188
     *      "data": "78297329329389e332234342"
189
     * }
190
     *
191
     * @apiSuccessExample {json} Success-Response (File updated, Last chunk):
192
     * HTTP/1.1 200 OK
193
     * {
194
     *      "status": 200,
195
     *      "data": 2
196
     * }
197
     *
198
     * @apiErrorExample {json} Error-Response (quota full):
199
     * HTTP/1.1 507 Insufficient Storage
200
     * {
201
     *      "status": 507
202
     *      "data": {
203
     *          "error": "Balloon\Exception\InsufficientStorage",
204
     *          "message": "user quota is full",
205
     *          "code": 66
206
     *      }
207
     * }
208
     *
209
     * @apiErrorExample {json} Error-Response (Size limit exceeded):
210
     * HTTP/1.1 400 Bad Request
211
     * {
212
     *      "status": 400,
213
     *      "data": {
214
     *          "error": "Balloon\\Exception\\Conflict",
215
     *          "message": "file size exceeded limit",
216
     *          "code": 17
217
     *      }
218
     * }
219
     *
220
     * @apiErrorExample {json} Error-Response (Chunks lost):
221
     * HTTP/1.1 400 Bad Request
222
     * {
223
     *      "status": 400,
224
     *      "data": {
225
     *          "error": "Balloon\\Exception\\Conflict",
226
     *          "message": "chunks lost, reupload all chunks",
227
     *          "code": 275
228
     *      }
229
     * }
230
     *
231
     * @apiErrorExample {json} Error-Response (Chunks invalid size):
232
     * HTTP/1.1 400 Bad Request
233
     * {
234
     *      "status": 400,
235
     *      "data": {
236
     *          "error": "Balloon\\Exception\\Conflict",
237
     *          "message": "merged chunks temp file size is not as expected",
238
     *          "code": 276
239
     *      }
240
     * }
241
     *
242
     * @param string $id
243
     * @param string $p
244
     * @param string $collection
245
     * @param string $name
246
     *
247
     * @return Response
248
     */
249
    public function putChunk(
250
        string $chunkgroup,
251
        ?string $id = null,
252
        ?string $p = null,
253
        ?string $collection = null,
254
        ?string $name = null,
255
        int $index = 1,
256
        int $chunks = 0,
257
        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...
258
        array $attributes = [],
259
        int $conflict = 0
260
    ) {
261
        ini_set('auto_detect_line_endings', '1');
262
        $input = fopen('php://input', 'rb');
263
        if (!is_string($chunkgroup) || empty($chunkgroup)) {
264
            throw new Exception\InvalidArgument('chunkgroup must be valid unique string');
265
        }
266
267
        if ($index > $chunks) {
268
            throw new Exception\InvalidArgument('chunk index can not be greater than the total number of chunks');
269
        }
270
271
        if (!preg_match('#^([A-Za-z0-9\.\-_])+$#', $chunkgroup)) {
272
            throw new Exception\InvalidArgument('chunkgroup may only contain #^[(A-Za-z0-9\.\-_])+$#');
273
        }
274
275
        $session = $this->db->selectCollection('fs.files')->findOne([
276
            'metadata.chunkgroup' => $this->server->getIdentity()->getId().'_'.$chunkgroup,
277
            'metadata.temporary' => true,
278
        ]);
279
280
        $parent = $this->getParent($id, $p, $collection);
281
282
        if ($session === null) {
283
            $session = $this->session_factory->add($this->server->getIdentity(), $parent, $input);
284
            $this->db->selectCollection('fs.files')->updateOne(
285
                ['_id' => $session->getId()],
286
                ['$set' => [
287
                    'metadata.chunkgroup' => $this->server->getIdentity()->getId().'_'.$chunkgroup,
288
                ]]
289
            );
290
        } else {
291
            $session = $session['_id'];
292
            $session = $this->session_factory->getOne($this->server->getIdentity(), $session);
293
            $this->session_factory->update($this->server->getIdentity(), $session, $parent, $input);
294
        }
295
296
        if ($index === $chunks) {
297
            $attributes = $this->_verifyAttributes($attributes);
298
299
            return $this->_put($session, $id, $p, $collection, $name, $attributes, $conflict);
300
        }
301
302
        return (new Response())->setCode(206)->setBody([
303
                'status' => 206,
304
                'data' => $index,
305
            ]);
306
    }
307
308
    /**
309
     * @api {put} /api/v1/file Upload file
310
     * @apiVersion 1.0.0
311
     * @apiName put
312
     * @apiGroup Node\File
313
     * @apiPermission none
314
     * @apiUse _getNode
315
     * @apiUse _conflictNode
316
     * @apiUse _writeAction
317
     *
318
     * @apiDescription Upload an entire file in one-shot. Attention, there is file size limit,
319
     * if you have possible big files use the method PUT chunk!
320
     *
321
     * @apiExample (cURL) example:
322
     * #Update content of file 544627ed3c58891f058b4686
323
     * curl -XPUT "https://SERVER/api/v1/file?id=544627ed3c58891f058b4686" --data-binary myfile.txt
324
     * curl -XPUT "https://SERVER/api/v1/file/544627ed3c58891f058b4686" --data-binary myfile.txt
325
     *
326
     * #Upload new file under collection 544627ed3c58891f058b3333
327
     * curl -XPUT "https://SERVER/api/v1/file?collection=544627ed3c58891f058b3333&name=myfile.txt" --data-binary myfile.txt
328
     *
329
     * @apiParam (GET Parameter) {string} [id] Either id, p (path) of a file node or a parent collection id must be given
330
     *
331
     * @apiParam (GET Parameter) {string} [id] Either id, p (path) of a file node or a parent collection id must be given
332
     * @apiParam (GET Parameter) {string} [p] Either id, p (path) of a file node or a parent collection id must be given
333
     * @apiParam (GET Parameter) {string} [collection] Either id, p (path) of a file node or a parent collection id must be given
334
     * (If none of them are given, the file will be placed to the root)
335
     * @apiParam (GET Parameter) {string} [name] Needs to be set if the chunk belongs to a new file
336
     * or to identify an existing child file if a collection id was set
337
     * @apiParam (GET Parameter) {object} attributes Overwrite some attributes which are usually generated on the server
338
     * @apiParam (GET Parameter) {number} attributes.created Set specific created timestamp (UNIX timestamp format)
339
     * @apiParam (GET Parameter) {number} attributes.changed Set specific changed timestamp (UNIX timestamp format)
340
     *
341
     * @apiSuccess (200 OK) {number} status Status Code
342
     * @apiSuccess (200 OK) {number} data Increased version number if an existing file was updated. It will return
343
     * the old version if the submited file content was equal to the existing one.
344
     *
345
     * @apiSuccess (201 Created) {number} status Status Code
346
     * @apiSuccess (201 Created) {string} data Node ID
347
     * @apiSuccessExample {json} Success-Response (New file created):
348
     * HTTP/1.1 201 Created
349
     * {
350
     *      "status": 201,
351
     *      "data": "78297329329389e332234342"
352
     * }
353
     *
354
     * @apiSuccessExample {json} Success-Response (File updated):
355
     * HTTP/1.1 200 OK
356
     * {
357
     *      "status": 200,
358
     *      "data": 2
359
     * }
360
     *
361
     * @apiErrorExample {json} Error-Response (quota full):
362
     * HTTP/1.1 507 Insufficient Storage
363
     * {
364
     *      "status": 507
365
     *      "data": {
366
     *          "error": "Balloon\Exception\InsufficientStorage",
367
     *          "message": "user quota is full",
368
     *          "code": 65
369
     *      }
370
     * }
371
     *
372
     * @apiErrorExample {json} Error-Response (Size limit exceeded):
373
     * HTTP/1.1 400 Bad Request
374
     * {
375
     *      "status": 400,
376
     *      "data": {
377
     *          "error": "Balloon\\Exception\\Conflict",
378
     *          "message": "file size exceeded limit",
379
     *          "code": 17
380
     *      }
381
     * }
382
     *
383
     * @param string $id
384
     * @param string $p
385
     * @param string $collection
386
     * @param string $name
387
     */
388
    public function put(
389
        ?string $id = null,
390
        ?string $p = null,
391
        ?string $collection = null,
392
        ?string $name = null,
393
        array $attributes = [],
394
        int $conflict = 0
395
    ): Response {
396
        $attributes = $this->_verifyAttributes($attributes);
397
398
        ini_set('auto_detect_line_endings', '1');
399
        $input = fopen('php://input', 'rb');
400
401
        $parent = $this->getParent($id, $p, $collection);
402
        $session = $this->session_factory->add($this->server->getIdentity(), $parent, $input);
403
404
        return $this->_put($session, $id, $p, $collection, $name, $attributes, $conflict);
405
    }
406
407
    /**
408
     * Get Parent.
409
     */
410
    protected function getParent($id, $p, $collection): Collection
411
    {
412
        if ($id !== null) {
413
            return $this->_getNode($id, $p)->getParent();
414
        }
415
        if ($p !== null) {
416
            $path = '/'.ltrim(dirname('/'.$p), '/');
417
418
            return $this->_getNode($id, $path, Collection::class);
419
        }
420
        if ($id === null && $p === null && $collection === null) {
421
            return $this->server->getFilesystem()->getRoot();
422
        }
423
424
        return $this->_getNode($collection, null, Collection::class);
425
    }
426
427
    /**
428
     * Add or update file.
429
     *
430
     * @param ObjecId $session
431
     * @param string  $id
432
     * @param string  $p
433
     * @param string  $collection
434
     * @param string  $name
435
     */
436
    protected function _put(
437
        SessionInterface $session,
438
        ?string $id = null,
439
        ?string $p = null,
440
        ?string $collection = null,
441
        ?string $name = null,
442
        array $attributes = [],
443
        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...
444
    ): Response {
445
        if (null === $id && null === $p && null === $name) {
446
            throw new Exception\InvalidArgument('neither id, p nor name was set');
447
        }
448
449
        if (null !== $p && null !== $name) {
450
            throw new Exception\InvalidArgument('p and name can not be used at the same time');
451
        }
452
453
        try {
454
            if (null !== $p) {
455
                $node = $this->_getNode(null, $p);
456
                $result = $node->setContent($session, $attributes);
457
458
                return (new Response())->setCode(200)->setBody([
459
                    'status' => 200,
460
                    'data' => $result,
461
                ]);
462
            }
463
            if (null !== $id && null === $collection) {
464
                $node = $this->_getNode($id);
465
                $result = $node->setContent($session, $attributes);
466
467
                return (new Response())->setCode(200)->setBody([
468
                    'status' => 200,
469
                    'data' => $result,
470
                ]);
471
            }
472
            if (null === $p && null === $id && null !== $name) {
473
                $collection = $this->_getNode($collection, null, Collection::class, false, true);
474
475
                if ($collection->childExists($name)) {
476
                    $child = $collection->getChild($name);
477
                    $result = $child->setContent($session, $attributes);
478
479
                    return (new Response())->setCode(200)->setBody([
480
                        'status' => 200,
481
                        'data' => $result,
482
                    ]);
483
                }
484
                if (!is_string($name) || empty($name)) {
485
                    throw new Exception\InvalidArgument('name must be a valid string');
486
                }
487
488
                $result = $collection->addFile($name, $session, $attributes)->getId();
489
490
                return (new Response())->setCode(201)->setBody([
491
                    'status' => 201,
492
                    'data' => (string) $result,
493
                ]);
494
            }
495
        } catch (ForbiddenException $e) {
496
            throw new Exception\Conflict('a node called '.$name.' does already exists in this collection', Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS);
497
        } catch (Exception\NotFound $e) {
498
            if (null !== $p && null === $id) {
499
                if (!is_string($p) || empty($p)) {
500
                    throw new Exception\InvalidArgument('path (p) must be a valid string');
501
                }
502
503
                $parent_path = '/'.ltrim(dirname('/'.$p), '/');
504
                $name = Helper::mb_basename($p);
505
506
                try {
507
                    $parent = $this->findNodeByPath($parent_path, Collection::class);
508
509
                    if (!is_string($name) || empty($name)) {
510
                        throw new Exception\InvalidArgument('name must be a valid string');
511
                    }
512
513
                    $result = $parent->addFile($name, $session, $attributes)->getId();
514
515
                    return (new Response())->setCode(201)->setBody([
516
                        'status' => 201,
517
                        'data' => (string) $result,
518
                    ]);
519
                } catch (Exception\NotFound $e) {
520
                    throw new Exception('parent collection '.$parent_path.' was not found');
521
                }
522
            } else {
523
                throw $e;
524
            }
525
        }
526
    }
527
}
528