Flag comparisons that always yield the same result
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\App\Api\Controller; |
||
15 | use Balloon\App\Api\v1\AttributeDecorator\DeltaDecorator; |
||
16 | use Balloon\App\Api\v1\AttributeDecorator\EventDecorator; |
||
17 | use Balloon\App\Api\v1\AttributeDecorator\NodeDecorator; |
||
18 | use Balloon\App\Api\v1\Collection as ApiCollection; |
||
19 | use Balloon\App\Api\v1\File as ApiFile; |
||
20 | use Balloon\Filesystem; |
||
21 | use Balloon\Filesystem\Exception; |
||
22 | use Balloon\Filesystem\Node\Collection; |
||
23 | use Balloon\Filesystem\Node\File; |
||
24 | use Balloon\Filesystem\Node\NodeInterface; |
||
25 | use Balloon\Helper; |
||
26 | use Balloon\Server; |
||
27 | use Balloon\Server\User; |
||
28 | use Closure; |
||
29 | use Generator; |
||
30 | use Micro\Http\Response; |
||
31 | use MongoDB\BSON\UTCDateTime; |
||
32 | use MongoDB\Database; |
||
33 | use Psr\Log\LoggerInterface; |
||
34 | use ZipStream\ZipStream; |
||
35 | |||
36 | class Node extends Controller |
||
37 | { |
||
38 | /** |
||
39 | * Filesystem. |
||
40 | * |
||
41 | * @var Filesystem |
||
42 | */ |
||
43 | protected $fs; |
||
44 | |||
45 | /** |
||
46 | * LoggerInterface. |
||
47 | * |
||
48 | * @var LoggerInterface |
||
49 | */ |
||
50 | protected $logger; |
||
51 | |||
52 | /** |
||
53 | * Server. |
||
54 | * |
||
55 | * @var Server |
||
56 | */ |
||
57 | protected $server; |
||
58 | |||
59 | /** |
||
60 | * User. |
||
61 | * |
||
62 | * @var User |
||
63 | */ |
||
64 | protected $user; |
||
65 | |||
66 | /** |
||
67 | * Decorator. |
||
68 | * |
||
69 | * @var NodeDecorator |
||
70 | */ |
||
71 | protected $node_decorator; |
||
72 | |||
73 | /** |
||
74 | * Database. |
||
75 | * |
||
76 | * @var Database |
||
77 | */ |
||
78 | protected $db; |
||
79 | |||
80 | /** |
||
81 | * Initialize. |
||
82 | */ |
||
83 | public function __construct(Server $server, NodeDecorator $decorator, LoggerInterface $logger, Database $db) |
||
84 | { |
||
85 | $this->fs = $server->getFilesystem(); |
||
86 | $this->user = $server->getIdentity(); |
||
87 | $this->server = $server; |
||
88 | $this->node_decorator = $decorator; |
||
89 | $this->logger = $logger; |
||
90 | $this->db = $db; |
||
91 | } |
||
92 | |||
93 | /** |
||
94 | * @api {head} /api/v1/node?id=:id Node exists? |
||
95 | * @apiVersion 1.0.0 |
||
96 | * @apiName head |
||
97 | * @apiGroup Node |
||
98 | * @apiPermission none |
||
99 | * @apiDescription Check if a node exists. Per default deleted nodes are ignore which means it will |
||
100 | * return a 404 if a deleted node is requested. You can change this behaviour via the deleted parameter. |
||
101 | * @apiUse _getNode |
||
102 | * |
||
103 | * @apiExample (cURL) example: |
||
104 | * curl -XHEAD "https://SERVER/api/v1/node?id=544627ed3c58891f058b4686" |
||
105 | * curl -XHEAD "https://SERVER/api/v1/node/544627ed3c58891f058b4686" |
||
106 | * curl -XHEAD "https://SERVER/api/v1/node?p=/absolute/path/to/my/node" |
||
107 | * |
||
108 | * @apiParam (GET Parameter) {number} [deleted=0] Wherever include deleted node or not, possible values:</br> |
||
109 | * - 0 Exclude deleted</br> |
||
110 | * - 1 Only deleted</br> |
||
111 | * - 2 Include deleted</br> |
||
112 | * |
||
113 | * @apiSuccessExample {json} Success-Response (Node does exist): |
||
114 | * HTTP/1.1 200 OK |
||
115 | * |
||
116 | * @apiSuccessExample {json} Success-Response (Node does not exist): |
||
117 | * HTTP/1.1 404 Not Found |
||
118 | * |
||
119 | * @param string $id |
||
120 | * @param string $p |
||
121 | */ |
||
122 | public function head(?string $id = null, ?string $p = null, int $deleted = 0): Response |
||
123 | { |
||
124 | try { |
||
125 | $result = $this->_getNode($id, $p, null, false, false, $deleted); |
||
126 | |||
127 | $response = (new Response()) |
||
128 | ->setHeader('Content-Length', (string) $result->getSize()) |
||
129 | ->setHeader('Content-Type', $result->getContentType()) |
||
130 | ->setCode(200); |
||
131 | |||
132 | return $response; |
||
133 | } catch (\Exception $e) { |
||
134 | return (new Response())->setCode(404); |
||
135 | } |
||
136 | } |
||
137 | |||
138 | /** |
||
139 | * @api {post} /api/v1/node/undelete?id=:id Undelete node |
||
140 | * @apiVersion 1.0.0 |
||
141 | * @apiName postUndelete |
||
142 | * @apiGroup Node |
||
143 | * @apiPermission none |
||
144 | * @apiDescription Undelete (Apiore from trash) a single node or multiple ones. |
||
145 | * @apiUse _getNodes |
||
146 | * @apiUse _conflictNode |
||
147 | * @apiUse _multiError |
||
148 | * @apiUse _writeAction |
||
149 | * |
||
150 | * @apiExample (cURL) example: |
||
151 | * curl -XPOST "https://SERVER/api/v1/node/undelete?id[]=544627ed3c58891f058b4686&id[]=544627ed3c58891f058b46865&pretty" |
||
152 | * curl -XPOST "https://SERVER/api/v1/node/undelete?id=544627ed3c58891f058b4686?pretty" |
||
153 | * curl -XPOST "https://SERVER/api/v1/node/544627ed3c58891f058b4686/undelete?conflict=2" |
||
154 | * curl -XPOST "https://SERVER/api/v1/node/undelete?p=/absolute/path/to/my/node&conflict=0&move=1&destid=544627ed3c58891f058b46889" |
||
155 | * |
||
156 | * @apiParam (GET Parameter) {string} [destid] Either destid or destp (path) of the new parent collection node must be given. |
||
157 | * @apiParam (GET Parameter) {string} [destp] Either destid or destp (path) of the new parent collection node must be given. |
||
158 | * |
||
159 | * @apiSuccessExample {json} Success-Response: |
||
160 | * HTTP/1.1 204 No Content |
||
161 | * |
||
162 | * @apiSuccessExample {json} Success-Response (conflict=1): |
||
163 | * HTTP/1.1 200 OK |
||
164 | * { |
||
165 | * "status":200, |
||
166 | * "data": "renamed (xy23)" |
||
167 | * } |
||
168 | * } |
||
169 | * |
||
170 | * @param array|string $id |
||
171 | * @param array|string $p |
||
172 | * @param string $destid |
||
173 | * @param string $destp |
||
174 | */ |
||
175 | public function postUndelete( |
||
176 | $id = null, |
||
177 | $p = null, |
||
178 | bool $move = false, |
||
179 | ?string $destid = null, |
||
180 | ?string $destp = null, |
||
181 | int $conflict = 0 |
||
182 | ): Response { |
||
183 | $parent = null; |
||
184 | if (true === $move) { |
||
185 | try { |
||
186 | $parent = $this->_getNode($destid, $destp, 'Collection', false, true); |
||
187 | } catch (Exception\NotFound $e) { |
||
188 | throw new Exception\NotFound( |
||
189 | 'destination collection was not found or is not a collection', |
||
190 | Exception\NotFound::DESTINATION_NOT_FOUND |
||
191 | ); |
||
192 | } |
||
193 | } |
||
194 | |||
195 | return $this->bulk($id, $p, function ($node) use ($parent, $conflict, $move) { |
||
196 | if (true === $move) { |
||
197 | $node = $node->setParent($parent, $conflict); |
||
198 | } |
||
199 | |||
200 | if ($node->isDeleted()) { |
||
201 | $node->undelete($conflict); |
||
202 | } |
||
203 | |||
204 | if (true === $move && NodeInterface::CONFLICT_RENAME === $conflict) { |
||
205 | return [ |
||
206 | 'status' => 200, |
||
207 | 'data' => [ |
||
208 | ], |
||
209 | ]; |
||
210 | } |
||
211 | |||
212 | return ['status' => 204]; |
||
213 | }); |
||
214 | } |
||
215 | |||
216 | /** |
||
217 | * @api {get} /api/v1/node?id=:id Download stream |
||
218 | * @apiVersion 1.0.0 |
||
219 | * @apiName get |
||
220 | * @apiGroup Node |
||
221 | * @apiPermission none |
||
222 | * @apiDescription Download node contents. Collections (Folder) are converted into |
||
223 | * a zip file in realtime. |
||
224 | * @apiUse _getNode |
||
225 | * |
||
226 | * @apiParam (GET Parameter) {number} [offset=0] Get content from a specific offset in bytes |
||
227 | * @apiParam (GET Parameter) {number} [length=0] Get content with until length in bytes reached |
||
228 | * @apiParam (GET Parameter) {string} [encode] Can be set to base64 to encode content as base64. |
||
229 | * @apiParam (GET Parameter) {boolean} [download=false] Force download file (Content-Disposition: attachment HTTP header) |
||
230 | * |
||
231 | * @apiExample (cURL) example: |
||
232 | * curl -XGET "https://SERVER/api/v1/node?id=544627ed3c58891f058b4686" > myfile.txt |
||
233 | * curl -XGET "https://SERVER/api/v1/node/544627ed3c58891f058b4686" > myfile.txt |
||
234 | * curl -XGET "https://SERVER/api/v1/node?p=/absolute/path/to/my/collection" > folder.zip |
||
235 | * |
||
236 | * @apiSuccessExample {string} Success-Response (encode=base64): |
||
237 | * HTTP/1.1 200 OK |
||
238 | * |
||
239 | * @apiSuccessExample {binary} Success-Response: |
||
240 | * HTTP/1.1 200 OK |
||
241 | * |
||
242 | * @apiErrorExample {json} Error-Response (Invalid offset): |
||
243 | * HTTP/1.1 400 Bad Request |
||
244 | * { |
||
245 | * "status": 400, |
||
246 | * "data": { |
||
247 | * "error": "Balloon\\Exception\\Conflict", |
||
248 | * "message": "invalid offset requested", |
||
249 | * "code": 277 |
||
250 | * } |
||
251 | * } |
||
252 | * |
||
253 | * @param array|string $id |
||
254 | * @param array|string $p |
||
255 | * @param string $encode |
||
256 | */ |
||
257 | public function get( |
||
258 | $id = null, |
||
259 | $p = null, |
||
260 | int $offset = 0, |
||
261 | int $length = 0, |
||
262 | ?string $encode = null, |
||
263 | bool $download = false, |
||
264 | string $name = 'selected' |
||
265 | ): ?Response { |
||
266 | if (is_array($id) || is_array($p)) { |
||
267 | return $this->combine($id, $p, $name); |
||
268 | } |
||
269 | |||
270 | $node = $this->_getNode($id, $p); |
||
271 | if ($node instanceof Collection) { |
||
272 | return (new Response())->setBody(function () use ($node) { |
||
273 | $node->getZip(); |
||
274 | }); |
||
275 | } |
||
276 | |||
277 | $response = new Response(); |
||
278 | |||
279 | if (true === $download) { |
||
280 | $response->setHeader('Content-Disposition', 'attachment; filename*=UTF-8\'\''.rawurlencode($name)); |
||
281 | $response->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0'); |
||
282 | $response->setHeader('Content-Type', 'application/octet-stream'); |
||
283 | $response->setHeader('Content-Length', (string) $node->getSize()); |
||
284 | $response->setHeader('Content-Transfer-Encoding', 'binary'); |
||
285 | } else { |
||
286 | $response->setHeader('Content-Disposition', 'inline; filename*=UTF-8\'\''.rawurlencode($name)); |
||
287 | } |
||
288 | |||
289 | return $response->setOutputFormat(null) |
||
290 | ->setBody(function () use ($node, $encode, $offset, $length) { |
||
291 | $mime = $node->getContentType(); |
||
292 | $stream = $node->get(); |
||
293 | $name = $node->getName(); |
||
294 | |||
295 | if (null === $stream) { |
||
296 | return; |
||
297 | } |
||
298 | |||
299 | if (0 !== $offset) { |
||
300 | if (fseek($stream, $offset) === -1) { |
||
301 | throw new Exception\Conflict( |
||
302 | 'invalid offset requested', |
||
303 | Exception\Conflict::INVALID_OFFSET |
||
304 | ); |
||
305 | } |
||
306 | } |
||
307 | |||
308 | $read = 0; |
||
309 | header('Content-Type: '.$mime.''); |
||
310 | if ('base64' === $encode) { |
||
311 | header('Content-Encoding: base64'); |
||
312 | while (!feof($stream)) { |
||
313 | if (0 !== $length && $read + 8192 > $length) { |
||
314 | echo base64_encode(fread($stream, $length - $read)); |
||
315 | exit(); |
||
316 | } |
||
317 | |||
318 | echo base64_encode(fread($stream, 8192)); |
||
319 | $read += 8192; |
||
320 | } |
||
321 | } else { |
||
322 | while (!feof($stream)) { |
||
323 | if (0 !== $length && $read + 8192 > $length) { |
||
324 | echo fread($stream, $length - $read); |
||
325 | exit(); |
||
326 | } |
||
327 | |||
328 | echo fread($stream, 8192); |
||
329 | $read += 8192; |
||
330 | } |
||
331 | } |
||
332 | }); |
||
333 | } |
||
334 | |||
335 | /** |
||
336 | * @api {post} /api/v1/node/readonly?id=:id Mark node as readonly |
||
337 | * @apiVersion 1.0.0 |
||
338 | * @apiName postReadonly |
||
339 | * @apiGroup Node |
||
340 | * @apiPermission none |
||
341 | * @apiDescription Mark (or unmark) node as readonly |
||
342 | * @apiUse _getNodes |
||
343 | * @apiUse _multiError |
||
344 | * @apiUse _writeAction |
||
345 | * |
||
346 | * @apiExample (cURL) example: |
||
347 | * curl -XPOST "https://SERVER/api/v1/node/readonly?id[]=544627ed3c58891f058b4686&id[]=544627ed3c58891f058b46865&readonly=1" |
||
348 | * curl -XPOST "https://SERVER/api/v1/node/544627ed3c58891f058b4686/readonly?readonly=0" |
||
349 | * curl -XPOST "https://SERVER/api/v1/node/readonly?p=/absolute/path/to/my/node" |
||
350 | * |
||
351 | * @apiParam (GET Parameter) {bool} [readonly=true] Set readonly to false to make node writeable again |
||
352 | * |
||
353 | * @apiSuccessExample {json} Success-Response: |
||
354 | * HTTP/1.1 204 No Content |
||
355 | * |
||
356 | * @param array|string $id |
||
357 | * @param array|string $p |
||
358 | */ |
||
359 | public function postReadonly($id = null, $p = null, bool $readonly = true): Response |
||
360 | { |
||
361 | return $this->bulk($id, $p, function ($node) use ($readonly) { |
||
362 | $node->setReadonly($readonly); |
||
363 | |||
364 | return ['status' => 204]; |
||
365 | }); |
||
366 | } |
||
367 | |||
368 | /** |
||
369 | * @apiDefine _nodeAttributes |
||
370 | * |
||
371 | * @apiSuccess (200 OK) {number} status Status Code |
||
372 | * @apiSuccess (200 OK) {object} data Attributes |
||
373 | * @apiSuccess (200 OK) {string} data.id Unique node id |
||
374 | * @apiSuccess (200 OK) {string} data.name Name |
||
375 | * @apiSuccess (200 OK) {string} data.hash MD5 content checksum (file node only) |
||
376 | * @apiSuccess (200 OK) {object} data.meta Extended meta attributes |
||
377 | * @apiSuccess (200 OK) {string} data.meta.description UTF-8 Text Description |
||
378 | * @apiSuccess (200 OK) {string} data.meta.color Color Tag (HEX) (Like: #000000) |
||
379 | * @apiSuccess (200 OK) {string} data.meta.author Author |
||
380 | * @apiSuccess (200 OK) {string} data.meta.mail Mail contact address |
||
381 | * @apiSuccess (200 OK) {string} data.meta.license License |
||
382 | * @apiSuccess (200 OK) {string} data.meta.copyright Copyright string |
||
383 | * @apiSuccess (200 OK) {string[]} data.meta.tags Search Tags |
||
384 | * @apiSuccess (200 OK) {number} data.size Size in bytes (Only file node), number of children if collection |
||
385 | * @apiSuccess (200 OK) {string} data.mime Mime type |
||
386 | * @apiSuccess (200 OK) {boolean} data.sharelink Is node shared? |
||
387 | * @apiSuccess (200 OK) {number} data.version File version (file node only) |
||
388 | * @apiSuccess (200 OK) {mixed} data.deleted Is boolean false if not deleted, if deleted it contains a deleted timestamp |
||
389 | * @apiSuccess (200 OK) {number} data.deleted.sec Unix timestamp |
||
390 | * @apiSuccess (200 OK) {number} data.deleted.usec Additional Microsecconds to Unix timestamp |
||
391 | * @apiSuccess (200 OK) {object} data.changed Changed timestamp |
||
392 | * @apiSuccess (200 OK) {number} data.changed.sec Unix timestamp |
||
393 | * @apiSuccess (200 OK) {number} data.changed.usec Additional Microsecconds to Unix timestamp |
||
394 | * @apiSuccess (200 OK) {object} data.created Created timestamp |
||
395 | * @apiSuccess (200 OK) {number} data.created.sec Unix timestamp |
||
396 | * @apiSuccess (200 OK) {number} data.created.usec Additional Microsecconds to Unix timestamp |
||
397 | * @apiSuccess (200 OK) {boolean} data.share Node is shared |
||
398 | * @apiSuccess (200 OK) {boolean} data.directory Is node a collection or a file? |
||
399 | * |
||
400 | * @apiSuccess (200 OK - additional attributes) {string} data.thumbnail Id of preview (file node only) |
||
401 | * @apiSuccess (200 OK - additional attributes) {string} data.access Access if node is shared, one of r/rw/w |
||
402 | * @apiSuccess (200 OK - additional attributes) {string} data.shareowner Username of the share owner |
||
403 | * @apiSuccess (200 OK - additional attributes) {string} data.parent ID of the parent node |
||
404 | * @apiSuccess (200 OK - additional attributes) {string} data.path Absolute node path |
||
405 | * @apiSuccess (200 OK - additional attributes) {boolean} data.filtered Node is filtered (usually only a collection) |
||
406 | * @apiSuccess (200 OK - additional attributes) {boolean} data.readonly Node is readonly |
||
407 | * |
||
408 | * @apiParam (GET Parameter) {string[]} [attributes] Filter attributes, per default not all attributes would be returned |
||
409 | * |
||
410 | * @param null|mixed $id |
||
411 | * @param null|mixed $p |
||
412 | */ |
||
413 | |||
414 | /** |
||
415 | * @api {get} /api/v1/node/attributes?id=:id Get attributes |
||
416 | * @apiVersion 1.0.0 |
||
417 | * @apiName getAttributes |
||
418 | * @apiGroup Node |
||
419 | * @apiPermission none |
||
420 | * @apiDescription Get attributes from one or multiple nodes |
||
421 | * @apiUse _getNode |
||
422 | * @apiUse _nodeAttributes |
||
423 | * |
||
424 | * @apiParam (GET Parameter) {string[]} [attributes] Filter attributes, per default only a bunch of attributes would be returned, if you |
||
425 | * need other attributes you have to request them (for example "path") |
||
426 | * |
||
427 | * @apiExample (cURL) example: |
||
428 | * curl -XGET "https://SERVER/api/v1/node/attributes?id=544627ed3c58891f058b4686&pretty" |
||
429 | * curl -XGET "https://SERVER/api/v1/node/attributes?id=544627ed3c58891f058b4686&attributes[0]=name&attributes[1]=deleted&pretty" |
||
430 | * curl -XGET "https://SERVER/api/v1/node/544627ed3c58891f058b4686/attributes?pretty" |
||
431 | * curl -XGET "https://SERVER/api/v1/node/attributes?p=/absolute/path/to/my/node&pretty" |
||
432 | * |
||
433 | * @apiSuccessExample {json} Success-Response: |
||
434 | * HTTP/1.1 200 OK |
||
435 | * { |
||
436 | * "status": 200, |
||
437 | * "data": { |
||
438 | * "id": "544627ed3c58891f058b4686", |
||
439 | * "name": "api.php", |
||
440 | * "hash": "a77f23ed800fd7a600a8c2cfe8cc370b", |
||
441 | * "meta": { |
||
442 | * "license": "GPLv3" |
||
443 | * }, |
||
444 | * "size": 178, |
||
445 | * "mime": "text\/plain", |
||
446 | * "sharelink": true, |
||
447 | * "version": 1, |
||
448 | * "deleted": false, |
||
449 | * "changed": { |
||
450 | * "sec": 1413883885, |
||
451 | * "usec": 869000 |
||
452 | * }, |
||
453 | * "created": { |
||
454 | * "sec": 1413883885, |
||
455 | * "usec": 869000 |
||
456 | * }, |
||
457 | * "share": false, |
||
458 | * "directory": false |
||
459 | * } |
||
460 | * } |
||
461 | * |
||
462 | * @param array|string $id |
||
463 | * @param array|string $p |
||
464 | */ |
||
465 | public function getAttributes($id = null, $p = null, array $attributes = []): Response |
||
466 | { |
||
467 | if (is_array($id) || is_array($p)) { |
||
468 | $nodes = []; |
||
469 | foreach ($this->_getNodes($id, $p) as $node) { |
||
470 | $nodes[] = $this->node_decorator->decorate($node, $attributes); |
||
471 | } |
||
472 | |||
473 | return (new Response())->setCode(200)->setBody([ |
||
474 | 'status' => 200, |
||
475 | 'data' => $nodes, |
||
476 | ]); |
||
477 | } |
||
478 | |||
479 | $result = $this->node_decorator->decorate($this->_getNode($id, $p), $attributes); |
||
480 | |||
481 | return (new Response())->setCode(200)->setBody([ |
||
482 | 'status' => 200, |
||
483 | 'data' => $result, |
||
484 | ]); |
||
485 | } |
||
486 | |||
487 | /** |
||
488 | * @api {get} /api/v1/node/parents?id=:id Get parent nodes |
||
489 | * @apiVersion 1.0.0 |
||
490 | * @apiName getParents |
||
491 | * @apiGroup Node |
||
492 | * @apiPermission none |
||
493 | * @apiDescription Get system attributes of all parent nodes. The hirarchy of all parent nodes is ordered in a |
||
494 | * single level array beginning with the collection on the highest level. |
||
495 | * @apiUse _getNode |
||
496 | * @apiUse _nodeAttributes |
||
497 | * @apiSuccess (200 OK) {object[]} data Nodes |
||
498 | * |
||
499 | * @apiParam (GET Parameter) {boolean} [self=true] Include requested collection itself at the end of the list (Will be ignored if the requested node is a file) |
||
500 | * |
||
501 | * @apiExample (cURL) example: |
||
502 | * curl -XGET "https://SERVER/api/v1/node/parents?id=544627ed3c58891f058b4686&pretty" |
||
503 | * curl -XGET "https://SERVER/api/v1/node/parents?id=544627ed3c58891f058b4686&attributes[0]=name&attributes[1]=deleted&pretty" |
||
504 | * curl -XGET "https://SERVER/api/v1/node/544627ed3c58891f058b4686/parents?pretty&self=1" |
||
505 | * curl -XGET "https://SERVER/api/v1/node/parents?p=/absolute/path/to/my/node&self=1" |
||
506 | * |
||
507 | * @apiSuccessExample {json} Success-Response: |
||
508 | * HTTP/1.1 200 OK |
||
509 | * { |
||
510 | * "status": 200, |
||
511 | * "data": [ |
||
512 | * { |
||
513 | * "id": "544627ed3c58891f058bbbaa", |
||
514 | * "name": "rootdir", |
||
515 | * "meta": {}, |
||
516 | * "size": 1, |
||
517 | * "mime": "inode\/directory", |
||
518 | * "deleted": false, |
||
519 | * "changed": { |
||
520 | * "sec": 1413883880, |
||
521 | * "usec": 869001 |
||
522 | * }, |
||
523 | * }, |
||
524 | * "created": { |
||
525 | * "sec": 1413883880, |
||
526 | * "usec": 869001 |
||
527 | * }, |
||
528 | * "share": false, |
||
529 | * "directory": true |
||
530 | * }, |
||
531 | * { |
||
532 | * "id": "544627ed3c58891f058b46cc", |
||
533 | * "name": "parentdir", |
||
534 | * "meta": {}, |
||
535 | * "size": 3, |
||
536 | * "mime": "inode\/directory", |
||
537 | * "deleted": false, |
||
538 | * "changed": { |
||
539 | * "sec": 1413883885, |
||
540 | * "usec": 869000 |
||
541 | * }, |
||
542 | * "created": { |
||
543 | * "sec": 1413883885, |
||
544 | * "usec": 869000 |
||
545 | * }, |
||
546 | * "share": false, |
||
547 | * "directory": true |
||
548 | * } |
||
549 | * ] |
||
550 | * } |
||
551 | * |
||
552 | * @param string $id |
||
553 | * @param string $p |
||
554 | */ |
||
555 | public function getParents(?string $id = null, ?string $p = null, array $attributes = [], bool $self = false): Response |
||
556 | { |
||
557 | $request = $this->_getNode($id, $p); |
||
558 | $parents = $request->getParents(); |
||
559 | $result = []; |
||
560 | |||
561 | if (true === $self && $request instanceof Collection) { |
||
562 | $result[] = $this->node_decorator->decorate($request, $attributes); |
||
563 | } |
||
564 | |||
565 | foreach ($parents as $node) { |
||
566 | $result[] = $this->node_decorator->decorate($node, $attributes); |
||
567 | } |
||
568 | |||
569 | return (new Response())->setCode(200)->setBody([ |
||
570 | 'status' => 200, |
||
571 | 'data' => $result, |
||
572 | ]); |
||
573 | } |
||
574 | |||
575 | /** |
||
576 | * @api {post} /api/v1/node/meta-attributes?id=:id Change meta attributes |
||
577 | * @apiVersion 1.0.0 |
||
578 | * @apiName postMetaAttributes |
||
579 | * @apiGroup Node |
||
580 | * @apiPermission none |
||
581 | * @apiDescription Change meta attributes of a node |
||
582 | * @apiUse _getNodes |
||
583 | * @apiUse _multiError |
||
584 | * |
||
585 | * @apiParam (GET Parameter) {string} [attributes.description] UTF-8 Text Description - Can contain anything as long as it is a string |
||
586 | * @apiParam (GET Parameter) {string} [attributes.color] Color Tag - Can contain anything as long as it is a string |
||
587 | * @apiParam (GET Parameter) {string} [attributes.author] Author - Can contain anything as long as it is a string |
||
588 | * @apiParam (GET Parameter) {string} [attributes.mail] Mail contact address - Can contain anything as long as it is a string |
||
589 | * @apiParam (GET Parameter) {string} [attributes.license] License - Can contain anything as long as it is a string |
||
590 | * @apiParam (GET Parameter) {string} [attributes.copyright] Copyright string - Can contain anything as long as it is a string |
||
591 | * @apiParam (GET Parameter) {string[]} [attributes.tags] Tags - Must be an array full of strings |
||
592 | * |
||
593 | * @apiExample (cURL) example: |
||
594 | * curl -XPOST "https://SERVER/api/v1/node/meta-attributes?id=544627ed3c58891f058b4686&author=peter.meier" |
||
595 | * curl -XPOST "https://SERVER/api/v1/node/544627ed3c58891f058b4686/meta-attributes?author=example" |
||
596 | * curl -XPOST "https://SERVER/api/v1/node/meta-attributes?p=/absolute/path/to/my/node?license=GPL-3.0" |
||
597 | * |
||
598 | * @apiSuccessExample {json} Success-Response: |
||
599 | * HTTP/1.1 204 No Content |
||
600 | * |
||
601 | * @param array|string $id |
||
602 | * @param array|string $p |
||
603 | */ |
||
604 | public function postMetaAttributes(?string $id = null, ?string $p = null): Response |
||
605 | { |
||
606 | return $this->bulk($id, $p, function ($node) { |
||
607 | $node->setMetaAttributes($_POST); |
||
608 | |||
609 | return ['status' => 204]; |
||
610 | }); |
||
611 | } |
||
612 | |||
613 | /** |
||
614 | * @api {post} /api/v1/node/name?id=:id Rename node |
||
615 | * @apiVersion 1.0.0 |
||
616 | * @apiName postName |
||
617 | * @apiGroup Node |
||
618 | * @apiPermission none |
||
619 | * @apiDescription Rename a node. The characters (\ < > : " / * ? |) (without the "()") are not allowed to use within a node name. |
||
620 | * @apiUse _getNode |
||
621 | * @apiUse _writeAction |
||
622 | * |
||
623 | * @apiParam (GET Parameter) {string} [name] The new name of the node |
||
624 | * @apiError (Error 400) Exception name contains invalid characters |
||
625 | * |
||
626 | * @apiExample (cURL) example: |
||
627 | * curl -XPOST "https://SERVER/api/v1/node/name?id=544627ed3c58891f058b4686&name=newname.txt" |
||
628 | * curl -XPOST "https://SERVER/api/v1/node/544627ed3c58891f058b4677/name?name=newdir" |
||
629 | * curl -XPOST "https://SERVER/api/v1/node/name?p=/absolute/path/to/my/node&name=newname.txt" |
||
630 | * |
||
631 | * @apiSuccessExample {json} Success-Response: |
||
632 | * HTTP/1.1 204 No Content |
||
633 | * |
||
634 | * @param string $id |
||
635 | * @param string $p |
||
636 | */ |
||
637 | public function postName(string $name, ?string $id = null, ?string $p = null): Response |
||
638 | { |
||
639 | $this->_getNode($id, $p)->setName($name); |
||
640 | |||
641 | return (new Response())->setCode(204); |
||
642 | } |
||
643 | |||
644 | /** |
||
645 | * @api {post} /api/v1/node/clone?id=:id Clone node |
||
646 | * @apiVersion 1.0.0 |
||
647 | * @apiName postClone |
||
648 | * @apiGroup Node |
||
649 | * @apiPermission none |
||
650 | * @apiDescription Clone a node |
||
651 | * @apiUse _getNode |
||
652 | * @apiUse _conflictNode |
||
653 | * @apiUse _multiError |
||
654 | * @apiUse _writeAction |
||
655 | * |
||
656 | * @apiParam (GET Parameter) {string} [destid] Either destid or destp (path) of the new parent collection node must be given. |
||
657 | * @apiParam (GET Parameter) {string} [destp] Either destid or destp (path) of the new parent collection node must be given. |
||
658 | * |
||
659 | * @apiExample (cURL) example: |
||
660 | * curl -XPOST "https://SERVER/api/v1/node/clone?id=544627ed3c58891f058b4686&dest=544627ed3c58891f058b4676" |
||
661 | * curl -XPOST "https://SERVER/api/v1/node/544627ed3c58891f058b4686/clone?dest=544627ed3c58891f058b4676&conflict=2" |
||
662 | * curl -XPOST "https://SERVER/api/v1/node/clone?p=/absolute/path/to/my/node&conflict=0&destp=/new/parent" |
||
663 | * |
||
664 | * @apiSuccessExample {json} Success-Response: |
||
665 | * HTTP/1.1 204 No Content |
||
666 | * |
||
667 | * @param array|string $id |
||
668 | * @param array|string $p |
||
669 | * @param string $destid |
||
670 | * @param string $destp |
||
671 | */ |
||
672 | public function postClone( |
||
673 | $id = null, |
||
674 | $p = null, |
||
675 | ?string $destid = null, |
||
676 | ?string $destp = null, |
||
677 | int $conflict = 0 |
||
678 | ): Response { |
||
679 | try { |
||
680 | $parent = $this->_getNode($destid, $destp, Collection::class, false, true); |
||
681 | } catch (Exception\NotFound $e) { |
||
682 | throw new Exception\NotFound( |
||
683 | 'destination collection was not found or is not a collection', |
||
684 | Exception\NotFound::DESTINATION_NOT_FOUND |
||
685 | ); |
||
686 | } |
||
687 | |||
688 | return $this->bulk($id, $p, function ($node) use ($parent, $conflict) { |
||
689 | $result = $node->copyTo($parent, $conflict); |
||
690 | |||
691 | return [ |
||
692 | 'status' => 201, |
||
693 | 'data' => $result, |
||
694 | ]; |
||
695 | }); |
||
696 | } |
||
697 | |||
698 | /** |
||
699 | * @api {post} /api/v1/node/move?id=:id Move node |
||
700 | * @apiVersion 1.0.0 |
||
701 | * @apiName postMove |
||
702 | * @apiGroup Node |
||
703 | * @apiPermission none |
||
704 | * @apiDescription Move node |
||
705 | * @apiUse _getNodes |
||
706 | * @apiUse _conflictNode |
||
707 | * @apiUse _multiError |
||
708 | * @apiUse _writeAction |
||
709 | * |
||
710 | * @apiParam (GET Parameter) {string} [destid] Either destid or destp (path) of the new parent collection node must be given. |
||
711 | * @apiParam (GET Parameter) {string} [destp] Either destid or destp (path) of the new parent collection node must be given. |
||
712 | * |
||
713 | * @apiExample (cURL) example: |
||
714 | * curl -XPOST "https://SERVER/api/v1/node/move?id=544627ed3c58891f058b4686?destid=544627ed3c58891f058b4655" |
||
715 | * curl -XPOST "https://SERVER/api/v1/node/544627ed3c58891f058b4686/move?destid=544627ed3c58891f058b4655" |
||
716 | * curl -XPOST "https://SERVER/api/v1/node/move?p=/absolute/path/to/my/node&destp=/new/parent&conflict=1 |
||
717 | * |
||
718 | * @apiSuccessExample {json} Success-Response: |
||
719 | * HTTP/1.1 204 No Content |
||
720 | * |
||
721 | * @apiSuccessExample {json} Success-Response (conflict=1): |
||
722 | * HTTP/1.1 200 OK |
||
723 | * { |
||
724 | * "status":200, |
||
725 | * "data": "renamed (xy23)" |
||
726 | * } |
||
727 | * |
||
728 | * @param array|string $id |
||
729 | * @param array|string $p |
||
730 | * @param string $destid |
||
731 | * @param string $destp |
||
732 | */ |
||
733 | public function postMove( |
||
734 | $id = null, |
||
735 | $p = null, |
||
736 | ?string $destid = null, |
||
737 | ?string $destp = null, |
||
738 | int $conflict = 0 |
||
739 | ): Response { |
||
740 | try { |
||
741 | $parent = $this->_getNode($destid, $destp, Collection::class, false, true); |
||
742 | } catch (Exception\NotFound $e) { |
||
743 | throw new Exception\NotFound( |
||
744 | 'destination collection was not found or is not a collection', |
||
745 | Exception\NotFound::DESTINATION_NOT_FOUND |
||
746 | ); |
||
747 | } |
||
748 | |||
749 | return $this->bulk($id, $p, function ($node) use ($parent, $conflict) { |
||
750 | $result = $node->setParent($parent, $conflict); |
||
751 | if (NodeInterface::CONFLICT_RENAME === $conflict) { |
||
752 | return [ |
||
753 | 'status' => 200, |
||
754 | 'data' => $node->getName(), |
||
755 | ]; |
||
756 | } |
||
757 | |||
758 | return [ |
||
759 | 'status' => 204, |
||
760 | ]; |
||
761 | }); |
||
762 | } |
||
763 | |||
764 | /** |
||
765 | * @api {delete} /api/v1/node?id=:id Delete node |
||
766 | * @apiVersion 1.0.0 |
||
767 | * @apiName delete |
||
768 | * @apiGroup Node |
||
769 | * @apiPermission none |
||
770 | * @apiDescription Delete node |
||
771 | * @apiUse _getNodes |
||
772 | * @apiUse _multiError |
||
773 | * @apiUse _writeAction |
||
774 | * |
||
775 | * @apiParam (GET Parameter) {boolean} [force=false] Force flag need to be set to delete a node from trash (node must have the deleted flag) |
||
776 | * @apiParam (GET Parameter) {boolean} [ignore_flag=false] If both ignore_flag and force_flag were set, the node will be deleted completely |
||
777 | * @apiParam (GET Parameter) {number} [at] Has to be a valid unix timestamp if so the node will destroy itself at this specified time instead immediatly |
||
778 | * |
||
779 | * @apiExample (cURL) example: |
||
780 | * curl -XDELETE "https://SERVER/api/v1/node?id=544627ed3c58891f058b4686" |
||
781 | * curl -XDELETE "https://SERVER/api/v1/node/544627ed3c58891f058b4686?force=1&ignore_flag=1" |
||
782 | * curl -XDELETE "https://SERVER/api/v1/node?p=/absolute/path/to/my/node" |
||
783 | * |
||
784 | * @apiSuccessExample {json} Success-Response: |
||
785 | * HTTP/1.1 204 No Content |
||
786 | * |
||
787 | * @param array|string $id |
||
788 | * @param array|string $p |
||
789 | * @param int $at |
||
790 | */ |
||
791 | public function delete( |
||
792 | $id = null, |
||
793 | $p = null, |
||
794 | bool $force = false, |
||
795 | bool $ignore_flag = false, |
||
796 | ?string $at = null |
||
797 | ): Response { |
||
798 | $failures = []; |
||
799 | |||
800 | if (null !== $at && '0' !== $at) { |
||
0 ignored issues
–
show
Unused Code
Bug
introduced
by
Loading history...
|
|||
801 | $at = $this->_verifyAttributes(['destroy' => $at])['destroy']; |
||
802 | } |
||
803 | |||
804 | return $this->bulk($id, $p, function ($node) use ($force, $ignore_flag, $at) { |
||
805 | if (null === $at) { |
||
806 | $node->delete($force && $node->isDeleted() || $force && $ignore_flag); |
||
807 | } else { |
||
808 | if ('0' === $at) { |
||
809 | $at = null; |
||
810 | } |
||
811 | $node->setDestroyable($at); |
||
812 | } |
||
813 | |||
814 | return [ |
||
815 | 'status' => 204, |
||
816 | ]; |
||
817 | }); |
||
818 | } |
||
819 | |||
820 | /** |
||
821 | * @api {get} /api/v1/node/query Custom query |
||
822 | * @apiVersion 1.0.0 |
||
823 | * @apiName getQuery |
||
824 | * @apiGroup Node |
||
825 | * @apiPermission none |
||
826 | * @apiDescription A custom query is similar requet to children. You do not have to provide any parent node (id or p) |
||
827 | * but you have to provide a filter therefore you can collect any nodes which do match the provided filter. It is a form of a search |
||
828 | * (search) but does not use the search engine like GET /node/search does. You can also create a persistent query collection, just look at |
||
829 | * POST /collection, there you can attach a filter option to the attributes paramater which would be the same as a custom query but just persistent. |
||
830 | * Since query parameters can only be strings and you perhaps would like to filter other data types, you have to send json as parameter to the server. |
||
831 | * @apiUse _nodeAttributes |
||
832 | * |
||
833 | * @apiExample (cURL) example: |
||
834 | * curl -XGET https://SERVER/api/v1/node/query?{%22filter%22:{%22shared%22:true,%22reference%22:{%22$exists%22:0}}} |
||
835 | * |
||
836 | * @apiParam (GET Parameter) {string[]} [attributes] Filter node attributes |
||
837 | * @apiParam (GET Parameter) {string[]} [filter] Filter nodes |
||
838 | * @apiParam (GET Parameter) {number} [deleted=0] Wherever include deleted nodes or not, possible values:</br> |
||
839 | * - 0 Exclude deleted</br> |
||
840 | * - 1 Only deleted</br> |
||
841 | * - 2 Include deleted</br> |
||
842 | * |
||
843 | * @apiSuccess (200 OK) {number} status Status Code |
||
844 | * @apiSuccess (200 OK) {object[]} data Children |
||
845 | * @apiSuccessExample {json} Success-Response: |
||
846 | * HTTP/1.1 200 OK |
||
847 | * { |
||
848 | * "status":200, |
||
849 | * "data": [{..}, {...}] //Shorted |
||
850 | * } |
||
851 | */ |
||
852 | public function getQuery(int $deleted = 0, array $filter = [], array $attributes = []): Response |
||
853 | { |
||
854 | $children = []; |
||
855 | $nodes = $this->fs->findNodesByFilterUser($deleted, $filter); |
||
856 | |||
857 | foreach ($nodes as $node) { |
||
858 | $child = $this->node_decorator->decorate($node, $attributes); |
||
859 | $children[] = $child; |
||
860 | } |
||
861 | |||
862 | return (new Response())->setCode(200)->setBody([ |
||
863 | 'status' => 200, |
||
864 | 'data' => $children, |
||
865 | ]); |
||
866 | } |
||
867 | |||
868 | /** |
||
869 | * @api {get} /api/v1/node/trash Get trash |
||
870 | * @apiName getTrash |
||
871 | * @apiVersion 1.0.0 |
||
872 | * @apiGroup Node |
||
873 | * @apiPermission none |
||
874 | * @apiDescription A similar endpoint to /api/v1/node/query filer={'deleted': {$type: 9}] but instead returning all deleted |
||
875 | * nodes (including children which are deleted as well) this enpoint only returns the first deleted node from every subtree) |
||
876 | * @apiUse _nodeAttributes |
||
877 | * |
||
878 | * @apiExample (cURL) example: |
||
879 | * curl -XGET https://SERVER/api/v1/node/trash?pretty |
||
880 | * |
||
881 | * @apiParam (GET Parameter) {string[]} [attributes] Filter node attributes |
||
882 | * |
||
883 | * @apiSuccess (200 OK) {number} status Status Code |
||
884 | * @apiSuccess (200 OK) {object[]} data Children |
||
885 | * @apiSuccessExample {json} Success-Response: |
||
886 | * HTTP/1.1 200 OK |
||
887 | * { |
||
888 | * "status":200, |
||
889 | * "data": [{..}, {...}] //Shorted |
||
890 | * } |
||
891 | */ |
||
892 | public function getTrash(array $attributes = []): Response |
||
893 | { |
||
894 | $children = []; |
||
895 | $nodes = $this->fs->findNodesByFilterUser(NodeInterface::DELETED_ONLY, ['deleted' => ['$type' => 9]]); |
||
896 | |||
897 | foreach ($nodes as $node) { |
||
898 | try { |
||
899 | $parent = $node->getParent(); |
||
900 | if (null !== $parent && $parent->isDeleted()) { |
||
901 | continue; |
||
902 | } |
||
903 | } catch (\Exception $e) { |
||
904 | } |
||
905 | |||
906 | $child = $this->node_decorator->decorate($node, $attributes); |
||
907 | $children[] = $child; |
||
908 | } |
||
909 | |||
910 | return (new Response())->setCode(200)->setBody([ |
||
911 | 'status' => 200, |
||
912 | 'data' => array_values($children), |
||
913 | ]); |
||
914 | } |
||
915 | |||
916 | /** |
||
917 | * @api {get} /api/v1/node/delta Get delta |
||
918 | * @apiVersion 1.0.0 |
||
919 | * @apiName getDelta |
||
920 | * @apiGroup Node |
||
921 | * @apiPermission none |
||
922 | * @apiUse _getNode |
||
923 | * |
||
924 | * @apiDescription Use this method to request a delta feed with all changes on the server (or) a snapshot of the server state. |
||
925 | * since the state of the submited cursor. If no cursor was submited the server will create one which can than be used to request any further deltas. |
||
926 | * If has_more is TRUE you need to request /delta immediatly again to |
||
927 | * receive the next bunch of deltas. If has_more is FALSE you should wait at least 120s seconds before any further requests to the |
||
928 | * api endpoint. You can also specify additional node attributes with the $attributes paramter or request the delta feed only for a specific node (see Get Attributes for that). |
||
929 | * If reset is TRUE you have to clean your local state because you will receive a snapshot of the server state, it is the same as calling the /delta endpoint |
||
930 | * without a cursor. reset could be TRUE if there was an account maintenance or a simialar case. |
||
931 | * You can request a different limit as well but be aware that the number of nodes could be slighty different from your requested limit. |
||
932 | * If requested with parameter id or p the delta gets generated recursively from the node given. |
||
933 | * |
||
934 | * @apiParam (GET Parameter) {number} [limit=250] Limit the number of delta entries, if too low you have to call this endpoint more often since has_more would be true more often |
||
935 | * @apiParam (GET Parameter) {string[]} [attributes] Filter attributes, per default not all attributes would be returned |
||
936 | * @apiParam (GET Parameter) {string} [cursor=null] Set a cursor to rquest next nodes within delta processing |
||
937 | * |
||
938 | * @apiExample (cURL) example: |
||
939 | * curl -XGET "https://SERVER/api/v1/node/delta?pretty" |
||
940 | * |
||
941 | * @apiSuccess (200 OK) {number} status Status Code |
||
942 | * @apiSuccess (200 OK) {object} data Delta feed |
||
943 | * @apiSuccess (200 OK) {boolean} data.reset If true the local state needs to be reseted, is alway TRUE during |
||
944 | * the first request to /delta without a cursor or in special cases like server or account maintenance |
||
945 | * @apiSuccess (200 OK) {string} data.cursor The cursor needs to be stored and reused to request further deltas |
||
946 | * @apiSuccess (200 OK) {boolean} data.has_more If has_more is TRUE /delta can be requested immediatly after the last request |
||
947 | * to receive further delta. If it is FALSE we should wait at least 120 seconds before any further delta requests to the api endpoint |
||
948 | * @apiSuccess (200 OK) {object[]} data.nodes Node list to process |
||
949 | * @apiSuccess (200 OK) {string} data.nodes.id Node ID |
||
950 | * @apiSuccess (200 OK) {string} data.nodes.deleted Is node deleted? |
||
951 | * @apiSuccess (200 OK) {object} data.nodes.changed Changed timestamp |
||
952 | * @apiSuccess (200 OK) {number} data.nodes.changed.sec Unix timestamp |
||
953 | * @apiSuccess (200 OK) {number} data.nodes.changed.usec Additional Microsecconds to Unix timestamp |
||
954 | * @apiSuccess (200 OK) {object} data.nodes.created Created timestamp (If data.nodes[].deleted is TRUE, created will be NULL) |
||
955 | * @apiSuccess (200 OK) {number} data.nodes.created.sec Unix timestamp |
||
956 | * @apiSuccess (200 OK) {number} data.nodes.created.usec Additional Microsecconds to Unix timestamp |
||
957 | * @apiSuccess (200 OK) {string} data.nodes.path The full absolute path to the node |
||
958 | * @apiSuccess (200 OK) {string} data.nodes.directory Is true if node is a directory |
||
959 | * @apiSuccessExample {json} Success-Response: |
||
960 | * HTTP/1.1 200 OK |
||
961 | * { |
||
962 | * "status": 200, |
||
963 | * "data": { |
||
964 | * "reset": false, |
||
965 | * "cursor": "aW5pdGlhbHwxMDB8NTc1YTlhMGIzYzU4ODkwNTE0OGI0NTZifDU3NWE5YTBiM2M1ODg5MDUxNDhiNDU2Yw==", |
||
966 | * "has_more": false, |
||
967 | * "nodes": [ |
||
968 | * { |
||
969 | * "id": "581afa783c5889ad7c8b4572", |
||
970 | * "deleted": true, |
||
971 | * "created": null, |
||
972 | * "changed": { |
||
973 | * "sec": 1478163064, |
||
974 | * "usec": 31.0.0 |
||
975 | * }, |
||
976 | * "path": "\/AAA\/AL", |
||
977 | * "directory": true |
||
978 | * }, |
||
979 | * { |
||
980 | * "id": "581afa783c5889ad7c8b3dcf", |
||
981 | * "deleted": false, |
||
982 | * "created": { |
||
983 | * "sec": 1478163048, |
||
984 | * "usec": 101000 |
||
985 | * }, |
||
986 | * "changed": { |
||
987 | * "sec": 1478163048, |
||
988 | * "usec": 101000 |
||
989 | * }, |
||
990 | * "path": "\/AL", |
||
991 | * "directory": true |
||
992 | * } |
||
993 | * ] |
||
994 | * } |
||
995 | * } |
||
996 | * |
||
997 | * @param string $id |
||
998 | * @param string $p |
||
999 | * @param string $cursor |
||
1000 | */ |
||
1001 | public function getDelta( |
||
1002 | DeltaDecorator $delta_decorator, |
||
1003 | ?string $id = null, |
||
1004 | ?string $p = null, |
||
1005 | ?string $cursor = null, |
||
1006 | int $limit = 250, |
||
1007 | array $attributes = [] |
||
1008 | ): Response { |
||
1009 | if (null !== $id || null !== $p) { |
||
1010 | $node = $this->_getNode($id, $p); |
||
1011 | } else { |
||
1012 | $node = null; |
||
1013 | } |
||
1014 | |||
1015 | $result = $this->fs->getDelta()->getDeltaFeed($cursor, $limit, $node); |
||
1016 | |||
1017 | $default = ['id', 'deleted', 'created', 'changed', 'path', 'directory']; |
||
1018 | $attributes = array_merge($default, $attributes); |
||
1019 | |||
1020 | foreach ($result['nodes'] as &$node) { |
||
1021 | if ($node instanceof NodeInterface) { |
||
1022 | $node = $this->node_decorator->decorate($node, $attributes); |
||
1023 | } else { |
||
1024 | $node = $delta_decorator->decorate($node, $attributes); |
||
1025 | } |
||
1026 | } |
||
1027 | |||
1028 | return (new Response())->setCode(200)->setBody([ |
||
1029 | 'status' => 200, |
||
1030 | 'data' => $result, |
||
1031 | ]); |
||
1032 | } |
||
1033 | |||
1034 | /** |
||
1035 | * @api {get} /api/v1/node/event-log?id=:id Event log |
||
1036 | * @apiVersion 1.0.0 |
||
1037 | * @apiName getEventLog |
||
1038 | * @apiGroup Node |
||
1039 | * @apiPermission none |
||
1040 | * @apiUse _getNode |
||
1041 | * @apiDescription Get detailed event log |
||
1042 | * Request all modifications which are made by the user himself or share members. |
||
1043 | * Possible operations are the follwing: |
||
1044 | * - deleteCollectionReference |
||
1045 | * - deleteCollectionShare |
||
1046 | * - deleteCollection |
||
1047 | * - addCollection |
||
1048 | * - addFile |
||
1049 | * - addCollectionShare |
||
1050 | * - addCollectionReference |
||
1051 | * - undeleteFile |
||
1052 | * - undeleteCollectionReference |
||
1053 | * - undeleteCollectionShare |
||
1054 | * - restoreFile |
||
1055 | * - renameFile |
||
1056 | * - renameCollection |
||
1057 | * - renameCollectionShare |
||
1058 | * - renameCollectionRFeference |
||
1059 | * - copyFile |
||
1060 | * - copyCollection |
||
1061 | * - copyCollectionShare |
||
1062 | * - copyCollectionRFeference |
||
1063 | * - moveFile |
||
1064 | * - moveCollection |
||
1065 | * - moveCollectionReference |
||
1066 | * - moveCollectionShare |
||
1067 | * |
||
1068 | * @apiExample (cURL) example: |
||
1069 | * curl -XGET "https://SERVER/api/v1/node/event-log?pretty" |
||
1070 | * curl -XGET "https://SERVER/api/v1/node/event-log?id=544627ed3c58891f058b4686&pretty" |
||
1071 | * curl -XGET "https://SERVER/api/v1/node/544627ed3c58891f058b4686/event-log?pretty&limit=10" |
||
1072 | * curl -XGET "https://SERVER/api/v1/node/event-log?p=/absolute/path/to/my/node&pretty" |
||
1073 | * |
||
1074 | * @apiParam (GET Parameter) {number} [limit=100] Sets limit of events to be returned |
||
1075 | * @apiParam (GET Parameter) {number} [skip=0] How many events are skiped (useful for paging) |
||
1076 | * |
||
1077 | * @apiSuccess (200 OK) {number} status Status Code |
||
1078 | * @apiSuccess (200 OK) {object[]} data Events |
||
1079 | * @apiSuccess (200 OK) {number} data.event Event ID |
||
1080 | * @apiSuccess (200 OK) {object} data.timestamp event timestamp |
||
1081 | * @apiSuccess (200 OK) {number} data.timestamp.sec Event timestamp timestamp in Unix time |
||
1082 | * @apiSuccess (200 OK) {number} data.timestamp.usec Additional microseconds to changed Unix timestamp |
||
1083 | * @apiSuccess (200 OK) {string} data.operation event operation (like addCollection, deleteFile, ...) |
||
1084 | * @apiSuccess (200 OK) {string} data.parent ID of the parent node at the time of the event |
||
1085 | * @apiSuccess (200 OK) {object} data.previous Previous state of actual data which has been modified during an event, can contain either version, name or parent |
||
1086 | * @apiSuccess (200 OK) {number} data.previous.version Version at the time before the event |
||
1087 | * @apiSuccess (200 OK) {string} data.previous.name Name at the time before the event |
||
1088 | * @apiSuccess (200 OK) {string} data.previous.parent Parent node at the time before the event |
||
1089 | * @apiSuccess (200 OK) {string} data.share If of the shared folder at the time of the event |
||
1090 | * @apiSuccess (200 OK) {string} data.name Name of the node at the time of the event |
||
1091 | * @apiSuccess (200 OK) {object} data.node Current data of the node (Not from the time of the event!) |
||
1092 | * @apiSuccess (200 OK) {boolean} data.node.deleted True if the node is deleted, false otherwise |
||
1093 | * @apiSuccess (200 OK) {string} data.node.id Actual ID of the node |
||
1094 | * @apiSuccess (200 OK) {string} data.node.name Current name of the node |
||
1095 | * @apiSuccess (200 OK) {object} data.user Data which contains information about the user who executed an event |
||
1096 | * @apiSuccess (200 OK) {string} data.user.id Actual user ID |
||
1097 | * @apiSuccess (200 OK) {string} data.user.username Current username of executed event |
||
1098 | * |
||
1099 | * @apiSuccessExample {json} Success-Response: |
||
1100 | * HTTP/1.1 200 OK |
||
1101 | * { |
||
1102 | * "status": 200, |
||
1103 | * "data": [ |
||
1104 | * { |
||
1105 | * "event": "57628e523c5889026f8b4570", |
||
1106 | * "timestamp": { |
||
1107 | * "sec": 1466076753, |
||
1108 | * "usec": 988000 |
||
1109 | * }, |
||
1110 | * "operation": "restoreFile", |
||
1111 | * "name": "file.txt", |
||
1112 | * "previous": { |
||
1113 | * "version": 16 |
||
1114 | * }, |
||
1115 | * "node": { |
||
1116 | * "id": "558c0b273c588963078b457a", |
||
1117 | * "name": "3dddsceheckfile.txt", |
||
1118 | * "deleted": false |
||
1119 | * }, |
||
1120 | * "parent": null, |
||
1121 | * "user": { |
||
1122 | * "id": "54354cb63c58891f058b457f", |
||
1123 | * "username": "gradmin.bzu" |
||
1124 | * }, |
||
1125 | * "share": null |
||
1126 | * } |
||
1127 | * ] |
||
1128 | * } |
||
1129 | * |
||
1130 | * @param string $id |
||
1131 | * @param string $p |
||
1132 | */ |
||
1133 | public function getEventLog(EventDecorator $event_decorator, ?string $id = null, ?string $p = null, int $skip = 0, int $limit = 100): Response |
||
1134 | { |
||
1135 | if (null !== $id || null !== $p) { |
||
1136 | $node = $this->_getNode($id, $p); |
||
1137 | } else { |
||
1138 | $node = null; |
||
1139 | } |
||
1140 | |||
1141 | $result = $this->fs->getDelta()->getEventLog($limit, $skip, $node); |
||
1142 | $body = []; |
||
1143 | foreach ($result as $event) { |
||
1144 | $body[] = $event_decorator->decorate($event); |
||
1145 | } |
||
1146 | |||
1147 | return (new Response())->setCode(200)->setBody([ |
||
1148 | 'status' => 200, |
||
1149 | 'data' => $body, |
||
1150 | ]); |
||
1151 | } |
||
1152 | |||
1153 | /** |
||
1154 | * @api {get} /api/v1/node/last-cursor Get last Cursor |
||
1155 | * @apiVersion 1.0.0 |
||
1156 | * @apiName geLastCursor |
||
1157 | * @apiGroup Node |
||
1158 | * @apiUse _getNode |
||
1159 | * @apiPermission none |
||
1160 | * @apiDescription Use this method to request the latest cursor if you only need to now |
||
1161 | * if there are changes on the server. This method will not return any other data than the |
||
1162 | * newest cursor. To request a feed with all deltas request /delta. |
||
1163 | * |
||
1164 | * @apiExample (cURL) example: |
||
1165 | * curl -XGET "https://SERVER/api/v1/node/last-cursor?pretty" |
||
1166 | * |
||
1167 | * @apiSuccess (200 OK) {number} status Status Code |
||
1168 | * @apiSuccess (200 OK) {string} data Newest cursor |
||
1169 | * @apiSuccessExample {json} Success-Response: |
||
1170 | * HTTP/1.1 200 OK |
||
1171 | * { |
||
1172 | * "status": 200, |
||
1173 | * "data": "aW5pdGlhbHwxMDB8NTc1YTlhMGIzYzU4ODkwNTE0OGI0NTZifDU3NWE5YTBiM2M1ODg5MDUxNDhiNDU2Yw==" |
||
1174 | * } |
||
1175 | * |
||
1176 | * @param string $id |
||
1177 | * @param string $p |
||
1178 | */ |
||
1179 | public function getLastCursor(?string $id = null, ?string $p = null): Response |
||
1180 | { |
||
1181 | if (null !== $id || null !== $p) { |
||
1182 | $node = $this->_getNode($id, $p); |
||
1183 | } else { |
||
1184 | $node = null; |
||
1185 | } |
||
1186 | |||
1187 | $result = $this->fs->getDelta()->getLastCursor(); |
||
1188 | |||
1189 | return (new Response())->setCode(200)->setBody([ |
||
1190 | 'status' => 200, |
||
1191 | 'data' => $result, |
||
1192 | ]); |
||
1193 | } |
||
1194 | |||
1195 | /** |
||
1196 | * Do bulk operations. |
||
1197 | * |
||
1198 | * @param array|string $id |
||
1199 | * @param array|string $p |
||
1200 | */ |
||
1201 | protected function bulk($id, $p, Closure $action): Response |
||
1202 | { |
||
1203 | if (is_array($id) || is_array($p)) { |
||
1204 | $errors = []; |
||
1205 | $body = []; |
||
1206 | |||
1207 | foreach ($this->_getNodes($id, $p) as $node) { |
||
1208 | try { |
||
1209 | $body[] = $action->call($this, $node); |
||
1210 | } catch (\Exception $e) { |
||
1211 | $errors[] = [ |
||
1212 | 'error' => get_class($e), |
||
1213 | 'message' => $e->getMessage(), |
||
1214 | 'status' => $e->getCode(), |
||
1215 | ]; |
||
1216 | } |
||
1217 | } |
||
1218 | |||
1219 | if (!empty($errors)) { |
||
1220 | return (new Response())->setCode(400)->setBody([ |
||
1221 | 'status' => 400, |
||
1222 | 'data' => $errors, |
||
1223 | ]); |
||
1224 | } |
||
1225 | if (empty($body)) { |
||
1226 | return (new Response())->setCode(204); |
||
1227 | } |
||
1228 | $body = array_shift($body); |
||
1229 | $response = (new Response())->setCode($body['status']); |
||
1230 | |||
1231 | if (isset($body['data'])) { |
||
1232 | $response->setBody([ |
||
1233 | 'status' => $body['status'], |
||
1234 | 'data' => $body['data'], |
||
1235 | ]); |
||
1236 | } |
||
1237 | |||
1238 | return $response; |
||
1239 | } |
||
1240 | |||
1241 | $body = $action->call($this, $this->_getNode($id, $p)); |
||
1242 | $response = (new Response())->setCode($body['status']); |
||
1243 | |||
1244 | if (isset($body['data'])) { |
||
1245 | $response->setBody([ |
||
1246 | 'status' => $body['status'], |
||
1247 | 'data' => $body['data'], |
||
1248 | ]); |
||
1249 | } |
||
1250 | |||
1251 | return $response; |
||
1252 | } |
||
1253 | |||
1254 | /** |
||
1255 | * Get node. |
||
1256 | * |
||
1257 | * @param string $id |
||
1258 | * @param string $path |
||
1259 | * @param string $class Force set node type |
||
1260 | * @param bool $multiple Allow $id to be an array |
||
1261 | * @param bool $allow_root Allow instance of root collection |
||
1262 | * @param bool $deleted How to handle deleted node |
||
1263 | */ |
||
1264 | protected function _getNode( |
||
1265 | ?string $id = null, |
||
1266 | ?string $path = null, |
||
1267 | ?string $class = null, |
||
1268 | bool $multiple = false, |
||
1269 | bool $allow_root = false, |
||
1270 | int $deleted = 2 |
||
1271 | ): NodeInterface { |
||
1272 | if (null === $class) { |
||
1273 | switch (get_class($this)) { |
||
1274 | case ApiFile::class: |
||
1275 | $class = File::class; |
||
1276 | |||
1277 | break; |
||
1278 | case ApiCollection::class: |
||
1279 | $class = Collection::class; |
||
1280 | |||
1281 | break; |
||
1282 | } |
||
1283 | } |
||
1284 | |||
1285 | return $this->fs->getNode($id, $path, $class, $multiple, $allow_root, $deleted); |
||
1286 | } |
||
1287 | |||
1288 | /** |
||
1289 | * Get nodes. |
||
1290 | * |
||
1291 | * @param string $id |
||
1292 | * @param string $path |
||
1293 | * @param string $class Force set node type |
||
1294 | * @param bool $deleted How to handle deleted node |
||
1295 | */ |
||
1296 | protected function _getNodes( |
||
1297 | $id = null, |
||
1298 | $path = null, |
||
1299 | ?string $class = null, |
||
1300 | int $deleted = 2 |
||
1301 | ): Generator { |
||
1302 | if (null === $class) { |
||
1303 | switch (get_class($this)) { |
||
1304 | case ApiFile::class: |
||
1305 | $class = File::class; |
||
1306 | |||
1307 | break; |
||
1308 | case ApiCollection::class: |
||
1309 | $class = Collection::class; |
||
1310 | |||
1311 | break; |
||
1312 | } |
||
1313 | } |
||
1314 | |||
1315 | return $this->fs->getNodes($id, $path, $class, $deleted); |
||
1316 | } |
||
1317 | |||
1318 | /** |
||
1319 | * Merge multiple nodes into one zip archive. |
||
1320 | * |
||
1321 | * @param string $id |
||
1322 | * @param string $path |
||
1323 | */ |
||
1324 | protected function combine($id = null, $path = null, string $name = 'selected') |
||
1325 | { |
||
1326 | $archive = new ZipStream($name.'.zip'); |
||
1327 | |||
1328 | foreach ($this->_getNodes($id, $path) as $node) { |
||
1329 | try { |
||
1330 | $node->zip($archive); |
||
1331 | } catch (\Exception $e) { |
||
1332 | $this->logger->debug('failed zip node in multi node request ['.$node->getId().']', [ |
||
1333 | 'category' => get_class($this), |
||
1334 | 'exception' => $e, |
||
1335 | ]); |
||
1336 | } |
||
1337 | } |
||
1338 | |||
1339 | $archive->finish(); |
||
1340 | } |
||
1341 | |||
1342 | /** |
||
1343 | * Check custom node attributes which have to be written. |
||
1344 | */ |
||
1345 | protected function _verifyAttributes(array $attributes): array |
||
1346 | { |
||
1347 | $valid_attributes = [ |
||
1348 | 'changed', |
||
1349 | 'destroy', |
||
1350 | 'created', |
||
1351 | 'meta', |
||
1352 | 'readonly', |
||
1353 | ]; |
||
1354 | |||
1355 | if ($this instanceof ApiCollection) { |
||
1356 | $valid_attributes[] = 'filter'; |
||
1357 | } |
||
1358 | |||
1359 | $check = array_merge(array_flip($valid_attributes), $attributes); |
||
1360 | |||
1361 | if ($this instanceof ApiCollection && count($check) > 6) { |
||
1362 | throw new Exception\InvalidArgument('Only changed, created, destroy timestamp, filter, readonly and/or meta attributes may be overwritten'); |
||
1363 | } |
||
1364 | if ($this instanceof ApiFile && count($check) > 5) { |
||
1365 | throw new Exception\InvalidArgument('Only changed, created, destroy timestamp, readonly and/or meta attributes may be overwritten'); |
||
1366 | } |
||
1367 | |||
1368 | foreach ($attributes as $attribute => $value) { |
||
1369 | switch ($attribute) { |
||
1370 | case 'filter': |
||
1371 | $attributes['filter'] = json_encode((array) $attributes['filter']); |
||
1372 | |||
1373 | break; |
||
1374 | case 'destroy': |
||
1375 | if (!Helper::isValidTimestamp($value)) { |
||
1376 | throw new Exception\InvalidArgument($attribute.' Changed timestamp must be valid unix timestamp'); |
||
1377 | } |
||
1378 | $attributes[$attribute] = new UTCDateTime($value.'000'); |
||
1379 | |||
1380 | break; |
||
1381 | case 'changed': |
||
1382 | case 'created': |
||
1383 | if (!Helper::isValidTimestamp($value)) { |
||
1384 | throw new Exception\InvalidArgument($attribute.' Changed timestamp must be valid unix timestamp'); |
||
1385 | } |
||
1386 | if ((int) $value > time()) { |
||
1387 | throw new Exception\InvalidArgument($attribute.' timestamp can not be set greater than the server time'); |
||
1388 | } |
||
1389 | $attributes[$attribute] = new UTCDateTime($value.'000'); |
||
1390 | |||
1391 | break; |
||
1392 | case 'readonly': |
||
1393 | $attributes['readonly'] = (bool) $attributes['readonly']; |
||
1394 | |||
1395 | break; |
||
1396 | } |
||
1397 | } |
||
1398 | |||
1399 | return $attributes; |
||
1400 | } |
||
1401 | } |
||
1402 |