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

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

parameters are used.

Unused Code Minor

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;
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);
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