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

Files::postRestore()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 5
nc 1
nop 3
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