Completed
Push — master ( 287393...7ff50d )
by Raffael
18:27 queued 14:12
created

src/app/Balloon.App.Api/v1/File.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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\Filesystem\Storage\Adapter\AdapterInterface as StorageAdapterInterface;
18
use Balloon\Helper;
19
use Micro\Http\Response;
20
use MongoDB\BSON\ObjectId;
21
22
class File extends Node
23
{
24
    /**
25
     * @api {get} /api/v1/file/history?id=:id Get history
26
     * @apiVersion 1.0.0
27
     * @apiName getHistory
28
     * @apiGroup Node\File
29
     * @apiPermission none
30
     * @apiDescription Get a full change history of a file
31
     * @apiUse _getNode
32
     *
33
     * @apiExample (cURL) example:
34
     * curl -XGET "https://SERVER/api/v1/file/history?id=544627ed3c58891f058b4686&pretty"
35
     * curl -XGET "https://SERVER/api/v1/file/544627ed3c58891f058b4686/history?pretty"
36
     * curl -XGET "https://SERVER/api/v1/file/history?p=/absolute/path/to/my/file&pretty"
37
     *
38
     * @apiSuccess (200 OK) {number} status Status Code
39
     * @apiSuccess (200 OK) {object[]} data History
40
     * @apiSuccess (200 OK) {number} data.version Version
41
     * @apiSuccess (200 OK) {object} data.changed Changed timestamp
42
     * @apiSuccess (200 OK) {number} data.changed.sec Changed timestamp in Unix time
43
     * @apiSuccess (200 OK) {number} data.changed.usec Additional microseconds to changed Unix timestamp
44
     * @apiSuccess (200 OK) {string} data.user User which changed the version
45
     * @apiSuccess (200 OK) {number} data.type Change type, there are five different change types including:</br>
46
     *  0 - Initially added</br>
47
     *  1 - Content modified</br>
48
     *  2 - Version rollback</br>
49
     *  3 - Deleted</br>
50
     *  4 - Undeleted
51
     * @apiSuccess (200 OK) {object} data.file Reference to the content
52
     * @apiSuccess (200 OK) {string} data.file.id Content reference ID
53
     * @apiSuccess (200 OK) {number} data.size Content size in bytes
54
     * @apiSuccess (200 OK) {string} data.mime Content mime type
55
     * @apiSuccessExample {json} Success-Response:
56
     * HTTP/1.1 200 OK
57
     * {
58
     *      "status": 200,
59
     *      "data": [
60
     *          {
61
     *              "version": 1,
62
     *              "changed": {
63
     *                  "sec": 1413883885,
64
     *                  "usec": 876000
65
     *              },
66
     *              "user": "peter.meier",
67
     *              "type": 0,
68
     *              "file": {
69
     *                  "$id": "544627ed3c58891f058b4688"
70
     *              },
71
     *              "size": 178,
72
     *              "mime": "text\/plain"
73
     *          }
74
     *      ]
75
     * }
76
     *
77
     * @param string $id
78
     * @param string $p
79
     */
80
    public function getHistory(?string $id = null, ?string $p = null): Response
81
    {
82
        $result = $this->_getNode($id, $p)->getHistory();
83
        $body = [];
84
85
        foreach ($result as $version) {
86
            $v = (array) $version;
87
88
            $v['user'] = $this->server->getUserById($version['user'])->getUsername();
89
            $v['changed'] = Helper::DateTimeToUnix($version['changed']);
90
            $body[] = $v;
91
        }
92
93
        return (new Response())->setCode(200)->setBody([
94
            'status' => 200,
95
            'data' => $body,
96
        ]);
97
    }
98
99
    /**
100
     * @api {post} /api/v1/file/restore?id=:id Rollback version
101
     * @apiVersion 1.0.0
102
     * @apiName postRestore
103
     * @apiGroup Node\File
104
     * @apiPermission none
105
     * @apiDescription Rollback to a recent version from history. Use the version number from history.
106
     * @apiUse _getNode
107
     *
108
     * @apiExample (cURL) example:
109
     * curl -XPOST "https://SERVER/api/v1/file/restore?id=544627ed3c58891f058b4686&pretty&vesion=11"
110
     * curl -XPOST "https://SERVER/api/v1/file/544627ed3c58891f058b4686/restore?pretty&version=1"
111
     * curl -XPOST "https://SERVER/api/v1/file/restore?p=/absolute/path/to/my/file&pretty&version=3"
112
     *
113
     * @apiParam (GET Parameter) {number} version The version from history to rollback to
114
     *
115
     * @apiSuccessExample {json} Success-Response:
116
     * HTTP/1.1 204 No Content
117
     *
118
     * @param string $id
119
     * @param string $p
120
     * @param string $version
121
     */
122
    public function postRestore(int $version, ?string $id = null, ?string $p = null): Response
123
    {
124
        $result = $this->_getNode($id, $p)->restore($version);
0 ignored issues
show
$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...
125
126
        return (new Response())->setCode(204);
127
    }
128
129
    /**
130
     * @api {put} /api/v1/file/chunk Upload file chunk
131
     * @apiVersion 1.0.0
132
     * @apiName putChunk
133
     * @apiGroup Node\File
134
     * @apiPermission none
135
     * @apiUse _getNode
136
     * @apuUse _conflictNode
137
     * @apiUse _writeAction
138
     * @apiDescription Upload a file chunk. Use this method if you have possible big files!
139
     * You have to manually splitt the binary data into
140
     * multiple chunks and upload them successively using this method. Once uploading the last chunk,
141
     * the server will automatically create or update the file node.
142
     * You may set the parent collection, name and or custom attributes only with the last request to save traffic.
143
     *
144
     * @apiExample (cURL) example:
145
     * # Upload a new file myfile.jpg into the collection 544627ed3c58891f058b4686.
146
     * 1. First splitt the file into multiple 8M (For example, you could also use a smaller or bigger size) chunks
147
     * 2. Create a unique name for the chunkgroup (Could also be the filename), best thing is to create a UUIDv4
148
     * 3. Upload each chunk successively (follow the binary order of your file!) using the chunk PUT method
149
     *   (The server identifies each chunk with the index parameter, beginning with #1).
150
     * 4. If chunk number 3 will be reached, the server automatically place all chunks to the new file node
151
     *
152
     * 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
153
     * 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
154
     * 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
155
     *
156
     * @apiParam (GET Parameter) {string} [id] Either id, p (path) of a file node or a parent collection id must be given
157
     * @apiParam (GET Parameter) {string} [p] Either id, p (path) of a file node or a parent collection id must be given
158
     * @apiParam (GET Parameter) {string} [collection] Either id, p (path) of a file node or a parent collection id must be given
159
     * (If none of them are given, the file will be placed to the root)
160
     * @apiParam (GET Parameter) {string} [name] Needs to be set if the chunk belongs to a new file
161
     * @apiParam (GET Parameter) {number} index Chunk ID (consider chunk order!)
162
     * @apiParam (GET Parameter) {number} chunks Total number of chunks
163
     * @apiParam (GET Parameter) {string} chunkgroup A unique name which identifes a group of chunks (One file)
164
     * @apiParam (GET Parameter) {number} size The total file size in bytes
165
     * @apiParam (GET Parameter) {object} [attributes] Overwrite some attributes which are usually generated on the server
166
     * @apiParam (GET Parameter) {number} [attributes.created] Set specific created timestamp (UNIX timestamp format)
167
     * @apiParam (GET Parameter) {number} [attributes.changed] Set specific changed timestamp (UNIX timestamp format)
168
     *
169
     *
170
     * @apiSuccess (200 OK) {number} status Status Code
171
     * @apiSuccess (200 OK) {number} data Increased version number if the last chunk was uploaded and existing node was updated.
172
     * It will return the old version if the submited file content was equal to the existing one.
173
     *
174
     * @apiSuccess (201 Created) {number} status Status Code
175
     * @apiSuccess (201 Created) {string} data Node ID if the last chunk was uploaded and a new node was added
176
     *
177
     * @apiSuccess (206 Partial Content) {number} status Status Code
178
     * @apiSuccess (206 Partial Content) {string} data Chunk ID if it was not the last chunk
179
     * @apiSuccessExample {json} Success-Response (Not the last chunk yet):
180
     * HTTP/1.1 206 Partial Content
181
     * {
182
     *      "status": 206,
183
     *      "data": "1"
184
     * }
185
     *
186
     * @apiSuccessExample {json} Success-Response (New file created, Last chunk):
187
     * HTTP/1.1 201 Created
188
     * {
189
     *      "status": 201,
190
     *      "data": "78297329329389e332234342"
191
     * }
192
     *
193
     * @apiSuccessExample {json} Success-Response (File updated, Last chunk):
194
     * HTTP/1.1 200 OK
195
     * {
196
     *      "status": 200,
197
     *      "data": 2
198
     * }
199
     *
200
     * @apiErrorExample {json} Error-Response (quota full):
201
     * HTTP/1.1 507 Insufficient Storage
202
     * {
203
     *      "status": 507
204
     *      "data": {
205
     *          "error": "Balloon\Exception\InsufficientStorage",
206
     *          "message": "user quota is full",
207
     *          "code": 66
208
     *      }
209
     * }
210
     *
211
     * @apiErrorExample {json} Error-Response (Size limit exceeded):
212
     * HTTP/1.1 400 Bad Request
213
     * {
214
     *      "status": 400,
215
     *      "data": {
216
     *          "error": "Balloon\\Exception\\Conflict",
217
     *          "message": "file size exceeded limit",
218
     *          "code": 17
219
     *      }
220
     * }
221
     *
222
     * @apiErrorExample {json} Error-Response (Chunks lost):
223
     * HTTP/1.1 400 Bad Request
224
     * {
225
     *      "status": 400,
226
     *      "data": {
227
     *          "error": "Balloon\\Exception\\Conflict",
228
     *          "message": "chunks lost, reupload all chunks",
229
     *          "code": 275
230
     *      }
231
     * }
232
     *
233
     * @apiErrorExample {json} Error-Response (Chunks invalid size):
234
     * HTTP/1.1 400 Bad Request
235
     * {
236
     *      "status": 400,
237
     *      "data": {
238
     *          "error": "Balloon\\Exception\\Conflict",
239
     *          "message": "merged chunks temp file size is not as expected",
240
     *          "code": 276
241
     *      }
242
     * }
243
     *
244
     * @param string $id
245
     * @param string $p
246
     * @param string $collection
247
     * @param string $name
248
     *
249
     * @return Response
250
     */
251
    public function putChunk(
252
        string $chunkgroup,
253
        ?string $id = null,
254
        ?string $p = null,
255
        ?string $collection = null,
256
        ?string $name = null,
257
        int $index = 1,
258
        int $chunks = 0,
259
        int $size = 0,
0 ignored issues
show
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...
260
        array $attributes = [],
261
        int $conflict = 0
262
    ) {
263
        ini_set('auto_detect_line_endings', '1');
264
        $input = fopen('php://input', 'rb');
265
        if (!is_string($chunkgroup) || empty($chunkgroup)) {
266
            throw new Exception\InvalidArgument('chunkgroup must be valid unique string');
267
        }
268
269
        if ($index > $chunks) {
270
            throw new Exception\InvalidArgument('chunk index can not be greater than the total number of chunks');
271
        }
272
273
        if (!preg_match('#^([A-Za-z0-9\.\-_])+$#', $chunkgroup)) {
274
            throw new Exception\InvalidArgument('chunkgroup may only contain #^[(A-Za-z0-9\.\-_])+$#');
275
        }
276
277
        $session = $this->db->selectCollection('fs.files')->findOne([
278
            'metadata.chunkgroup' => $this->server->getIdentity()->getId().'_'.$chunkgroup,
279
        ]);
280
281
        $storage = $this->getStorage($id, $p, $collection);
282
283
        if ($session === null) {
284
            $session = $storage->storeTemporaryFile($input, $this->server->getIdentity());
285
            $this->db->selectCollection('fs.files')->updateOne(
286
                ['_id' => $session],
287
                ['$set' => [
288
                    'metadata.chunkgroup' => $this->server->getIdentity()->getId().'_'.$chunkgroup,
289
                ]]
290
            );
291
        } else {
292
            $session = $session['_id'];
293
            $storage->storeTemporaryFile($input, $this->server->getIdentity(), $session);
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
        $storage = $this->getStorage($id, $p, $collection);
402
        $session = $storage->storeTemporaryFile($input, $this->server->getIdentity());
403
404
        return $this->_put($session, $id, $p, $collection, $name, $attributes, $conflict);
405
    }
406
407
    /**
408
     * Get storage.
409
     */
410
    protected function getStorage($id, $p, $collection): StorageAdapterInterface
411
    {
412
        if ($id !== null) {
413
            return $this->_getNode($id, $p)->getParent()->getStorage();
414
        }
415
        if ($p !== null) {
416
            $path = '/'.ltrim(dirname('/'.$p), '/');
417
418
            return $this->_getNode($id, $path, Collection::class)->getStorage();
419
        }
420
        if ($id === null && $p === null && $collection === null) {
421
            return $this->server->getFilesystem()->getRoot()->getStorage();
422
        }
423
424
        return $this->_getNode($collection, null, Collection::class)->getStorage();
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
        ObjectId $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
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(
497
                'a node called '.$name.' does already exists in this collection',
498
                Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS
499
            );
500
        } catch (Exception\NotFound $e) {
501
            if (null !== $p && null === $id) {
502
                if (!is_string($p) || empty($p)) {
503
                    throw new Exception\InvalidArgument('path (p) must be a valid string');
504
                }
505
506
                $parent_path = '/'.ltrim(dirname('/'.$p), '/');
507
                $name = basename($p);
508
509
                try {
510
                    $parent = $this->fs->findNodeByPath($parent_path, Collection::class);
511
512
                    if (!is_string($name) || empty($name)) {
513
                        throw new Exception\InvalidArgument('name must be a valid string');
514
                    }
515
516
                    $result = $parent->addFile($name, $session, $attributes)->getId();
517
518
                    return (new Response())->setCode(201)->setBody([
519
                        'status' => 201,
520
                        'data' => (string) $result,
521
                    ]);
522
                } catch (Exception\NotFound $e) {
523
                    throw new Exception('parent collection '.$parent_path.' was not found');
524
                }
525
            } else {
526
                throw $e;
527
            }
528
        }
529
    }
530
}
531