1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* balloon |
7
|
|
|
* |
8
|
|
|
* @copyright Copryright (c) 2012-2018 gyselroth GmbH (https://gyselroth.com) |
9
|
|
|
* @license GPL-3.0 https://opensource.org/licenses/GPL-3.0 |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Balloon\Filesystem\Node; |
13
|
|
|
|
14
|
|
|
use Balloon\Filesystem; |
15
|
|
|
use Balloon\Filesystem\Acl; |
16
|
|
|
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException; |
17
|
|
|
use Balloon\Filesystem\Exception; |
18
|
|
|
use Balloon\Filesystem\Storage; |
19
|
|
|
use Balloon\Hook; |
20
|
|
|
use Balloon\Mime; |
21
|
|
|
use Balloon\Server\User; |
22
|
|
|
use MongoDB\BSON\UTCDateTime; |
23
|
|
|
use Psr\Log\LoggerInterface; |
24
|
|
|
use Sabre\DAV\IFile; |
25
|
|
|
|
26
|
|
|
class File extends AbstractNode implements IFile |
27
|
|
|
{ |
28
|
|
|
/** |
29
|
|
|
* History types. |
30
|
|
|
*/ |
31
|
|
|
const HISTORY_CREATE = 0; |
32
|
|
|
const HISTORY_EDIT = 1; |
33
|
|
|
const HISTORY_RESTORE = 2; |
34
|
|
|
const HISTORY_DELETE = 3; |
35
|
|
|
const HISTORY_UNDELETE = 4; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* Empty content hash (NULL). |
39
|
|
|
*/ |
40
|
|
|
const EMPTY_CONTENT = 'd41d8cd98f00b204e9800998ecf8427e'; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Temporary file patterns. |
44
|
|
|
* |
45
|
|
|
* @param array |
46
|
|
|
**/ |
47
|
|
|
protected $temp_files = [ |
48
|
|
|
'/^\._(.*)$/', // OS/X resource forks |
49
|
|
|
'/^.DS_Store$/', // OS/X custom folder settings |
50
|
|
|
'/^desktop.ini$/', // Windows custom folder settings |
51
|
|
|
'/^Thumbs.db$/', // Windows thumbnail cache |
52
|
|
|
'/^.(.*).swpx$/', // ViM temporary files |
53
|
|
|
'/^.(.*).swx$/', // ViM temporary files |
54
|
|
|
'/^.(.*).swp$/', // ViM temporary files |
55
|
|
|
'/^\.dat(.*)$/', // Smultron seems to create these |
56
|
|
|
'/^~lock.(.*)#$/', // Windows 7 lockfiles |
57
|
|
|
]; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* MD5 Hash of the content. |
61
|
|
|
* |
62
|
|
|
* @var string |
63
|
|
|
*/ |
64
|
|
|
protected $hash; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* File version. |
68
|
|
|
* |
69
|
|
|
* @var int |
70
|
|
|
*/ |
71
|
|
|
protected $version = 0; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* File size. |
75
|
|
|
* |
76
|
|
|
* @var int |
77
|
|
|
*/ |
78
|
|
|
protected $size = 0; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* History. |
82
|
|
|
* |
83
|
|
|
* @var array |
84
|
|
|
*/ |
85
|
|
|
protected $history = []; |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Storage. |
89
|
|
|
* |
90
|
|
|
* @var Storage |
91
|
|
|
*/ |
92
|
|
|
protected $_storage; |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* Storage attributes. |
96
|
|
|
* |
97
|
|
|
* @var mixed |
98
|
|
|
*/ |
99
|
|
|
protected $storage; |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* Initialize file node. |
103
|
|
|
* |
104
|
|
|
* @param array $attributes |
105
|
|
|
* @param Filesystem $fs |
106
|
|
|
*/ |
107
|
|
|
public function __construct(array $attributes, Filesystem $fs, LoggerInterface $logger, Hook $hook, Acl $acl, Storage $storage) |
108
|
|
|
{ |
109
|
|
|
$this->_fs = $fs; |
110
|
|
|
$this->_server = $fs->getServer(); |
111
|
|
|
$this->_db = $fs->getDatabase(); |
112
|
|
|
$this->_user = $fs->getUser(); |
113
|
|
|
$this->_logger = $logger; |
114
|
|
|
$this->_hook = $hook; |
115
|
|
|
$this->_storage = $storage; |
116
|
|
|
$this->_acl = $acl; |
117
|
|
|
|
118
|
|
|
foreach ($attributes as $attr => $value) { |
119
|
|
|
$this->{$attr} = $value; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
$this->raw_attributes = $attributes; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Read content and return ressource. |
127
|
|
|
* |
128
|
|
|
* @return resource |
129
|
|
|
*/ |
130
|
|
|
public function get() |
131
|
|
|
{ |
132
|
|
|
try { |
133
|
|
|
if (null === $this->storage) { |
134
|
|
|
return null; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
return $this->_storage->getFile($this, $this->storage); |
138
|
|
|
} catch (\Exception $e) { |
139
|
|
|
throw new Exception\NotFound( |
140
|
|
|
'content not found', |
141
|
|
|
Exception\NotFound::CONTENTS_NOT_FOUND |
142
|
|
|
); |
143
|
|
|
} |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Copy node. |
148
|
|
|
* |
149
|
|
|
* @param Collection $parent |
150
|
|
|
* @param int $conflict |
151
|
|
|
* @param string $recursion |
152
|
|
|
* @param bool $recursion_first |
153
|
|
|
* |
154
|
|
|
* @return NodeInterface |
155
|
|
|
*/ |
156
|
|
|
public function copyTo(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true): NodeInterface |
157
|
|
|
{ |
158
|
|
|
$this->_hook->run( |
159
|
|
|
'preCopyFile', |
160
|
|
|
[$this, $parent, &$conflict, &$recursion, &$recursion_first] |
161
|
|
|
); |
162
|
|
|
|
163
|
|
|
if (NodeInterface::CONFLICT_RENAME === $conflict && $parent->childExists($this->name)) { |
164
|
|
|
$name = $this->getDuplicateName(); |
165
|
|
|
} else { |
166
|
|
|
$name = $this->name; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
if (NodeInterface::CONFLICT_MERGE === $conflict && $parent->childExists($this->name)) { |
170
|
|
|
$result = $parent->getChild($this->name); |
171
|
|
|
$result->put($this->get()); |
172
|
|
|
} else { |
173
|
|
|
$result = $parent->addFile($name, $this->get(), [ |
174
|
|
|
'created' => $this->created, |
175
|
|
|
'changed' => $this->changed, |
176
|
|
|
'deleted' => $this->deleted, |
177
|
|
|
'meta' => $this->meta, |
178
|
|
|
], NodeInterface::CONFLICT_NOACTION, true); |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
$this->_hook->run( |
182
|
|
|
'postCopyFile', |
183
|
|
|
[$this, $parent, $result, $conflict, $recursion, $recursion_first] |
184
|
|
|
); |
185
|
|
|
|
186
|
|
|
return $result; |
187
|
|
|
} |
188
|
|
|
|
189
|
|
|
/** |
190
|
|
|
* Get history. |
191
|
|
|
* |
192
|
|
|
* @return array |
193
|
|
|
*/ |
194
|
|
|
public function getHistory(): array |
195
|
|
|
{ |
196
|
|
|
return $this->history; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Restore content to some older version. |
201
|
|
|
* |
202
|
|
|
* @param int $version |
203
|
|
|
* |
204
|
|
|
* @return bool |
205
|
|
|
*/ |
206
|
|
|
public function restore(int $version): bool |
207
|
|
|
{ |
208
|
|
|
if (!$this->_acl->isAllowed($this, 'w')) { |
209
|
|
|
throw new ForbiddenException( |
210
|
|
|
'not allowed to restore node '.$this->name, |
211
|
|
|
ForbiddenException::NOT_ALLOWED_TO_RESTORE |
212
|
|
|
); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
$this->_hook->run('preRestoreFile', [$this, &$version]); |
216
|
|
|
|
217
|
|
|
if ($this->readonly) { |
218
|
|
|
throw new Exception\Conflict( |
219
|
|
|
'node is marked as readonly, it is not possible to change any content', |
220
|
|
|
Exception\Conflict::READONLY |
221
|
|
|
); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
if ($this->version === $version) { |
225
|
|
|
throw new Exception('file is already version '.$version); |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
$v = array_search($version, array_column($this->history, 'version'), true); |
229
|
|
|
if (null === $v) { |
230
|
|
|
throw new Exception('failed restore file to version '.$version.', version was not found'); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
$file = $this->history[$v]['storage']; |
234
|
|
|
/*$exists = []; |
|
|
|
|
235
|
|
|
|
236
|
|
|
if (null !== $file) { |
237
|
|
|
try { |
238
|
|
|
$exists = $this->_storage->getFileMeta($this, $this->history[$v]['storage']); |
239
|
|
|
} catch (\Exception $e) { |
240
|
|
|
throw new Exception('could not restore to version '.$version.', version content does not exists'); |
241
|
|
|
} |
242
|
|
|
}*/ |
243
|
|
|
|
244
|
|
|
$current = $this->version; |
|
|
|
|
245
|
|
|
$new = $this->increaseVersion(); |
246
|
|
|
|
247
|
|
|
$this->history[] = [ |
248
|
|
|
'version' => $new, |
249
|
|
|
'changed' => $this->changed, |
250
|
|
|
'user' => $this->owner, |
251
|
|
|
'type' => self::HISTORY_RESTORE, |
252
|
|
|
'hash' => $this->history[$v]['hash'], |
253
|
|
|
'origin' => $this->history[$v]['version'], |
254
|
|
|
'storage' => $this->history[$v]['storage'], |
255
|
|
|
'storage_adapter' => $this->history[$v]['storage_adapter'], |
256
|
|
|
'size' => $this->history[$v]['size'], |
257
|
|
|
'mime' => isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : null, |
258
|
|
|
]; |
259
|
|
|
|
260
|
|
|
try { |
261
|
|
|
$this->deleted = false; |
262
|
|
|
$this->version = $new; |
263
|
|
|
$this->storage = $this->history[$v]['storage']; |
264
|
|
|
$this->storage_adapter = $this->history[$v]['storage_adapter']; |
265
|
|
|
|
266
|
|
|
$this->hash = null === $file ? self::EMPTY_CONTENT : $this->history[$v]['hash']; |
267
|
|
|
$this->mime = isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : null; |
268
|
|
|
$this->size = $this->history[$v]['size']; |
269
|
|
|
$this->changed = $this->history[$v]['changed']; |
270
|
|
|
|
271
|
|
|
$this->save([ |
272
|
|
|
'deleted', |
273
|
|
|
'version', |
274
|
|
|
'storage', |
275
|
|
|
'storage_adapter', |
276
|
|
|
'hash', |
277
|
|
|
'mime', |
278
|
|
|
'size', |
279
|
|
|
'history', |
280
|
|
|
'changed', |
281
|
|
|
]); |
282
|
|
|
|
283
|
|
|
$this->_hook->run('postRestoreFile', [$this, &$version]); |
284
|
|
|
|
285
|
|
|
$this->_logger->info('restored file ['.$this->_id.'] to version ['.$version.']', [ |
286
|
|
|
'category' => get_class($this), |
287
|
|
|
]); |
288
|
|
|
} catch (\Exception $e) { |
289
|
|
|
$this->_logger->error('failed restore file ['.$this->_id.'] to version ['.$version.']', [ |
290
|
|
|
'category' => get_class($this), |
291
|
|
|
'exception' => $e, |
292
|
|
|
]); |
293
|
|
|
|
294
|
|
|
throw $e; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
return true; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Delete node. |
302
|
|
|
* |
303
|
|
|
* Actually the node will not be deleted (Just set a delete flag), set $force=true to |
304
|
|
|
* delete finally |
305
|
|
|
* |
306
|
|
|
* @param bool $force |
307
|
|
|
* @param string $recursion |
308
|
|
|
* @param bool $recursion_first |
309
|
|
|
* |
310
|
|
|
* @return bool |
311
|
|
|
*/ |
312
|
|
|
public function delete(bool $force = false, ?string $recursion = null, bool $recursion_first = true): bool |
313
|
|
|
{ |
314
|
|
|
if (!$this->_acl->isAllowed($this, 'w')) { |
315
|
|
|
throw new ForbiddenException( |
316
|
|
|
'not allowed to delete node '.$this->name, |
317
|
|
|
ForbiddenException::NOT_ALLOWED_TO_DELETE |
318
|
|
|
); |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
$this->_hook->run('preDeleteFile', [$this, &$force, &$recursion, &$recursion_first]); |
322
|
|
|
|
323
|
|
|
if ($this->readonly && null !== $this->_user) { |
324
|
|
|
throw new Exception\Conflict( |
325
|
|
|
'node is marked as readonly, it is not possible to delete it', |
326
|
|
|
Exception\Conflict::READONLY |
327
|
|
|
); |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
if (true === $force || $this->isTemporaryFile()) { |
331
|
|
|
$result = $this->_forceDelete(); |
332
|
|
|
$this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]); |
333
|
|
|
|
334
|
|
|
return $result; |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
$ts = new UTCDateTime(); |
338
|
|
|
$this->deleted = $ts; |
339
|
|
|
$this->increaseVersion(); |
340
|
|
|
|
341
|
|
|
$this->history[] = [ |
342
|
|
|
'version' => $this->version, |
343
|
|
|
'changed' => $ts, |
344
|
|
|
'user' => ($this->_user === null) ? null : $this->_user->getId(), |
345
|
|
|
'type' => self::HISTORY_DELETE, |
346
|
|
|
'storage' => $this->storage, |
347
|
|
|
'storage_adapter' => $this->storage_adapter, |
348
|
|
|
'size' => $this->size, |
349
|
|
|
'hash' => $this->hash, |
350
|
|
|
]; |
351
|
|
|
|
352
|
|
|
$result = $this->save([ |
353
|
|
|
'version', |
354
|
|
|
'deleted', |
355
|
|
|
'history', |
356
|
|
|
], [], $recursion, $recursion_first); |
357
|
|
|
|
358
|
|
|
$this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]); |
359
|
|
|
|
360
|
|
|
return $result; |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* Check if file is temporary. |
365
|
|
|
* |
366
|
|
|
* @return bool |
367
|
|
|
**/ |
368
|
|
|
public function isTemporaryFile(): bool |
369
|
|
|
{ |
370
|
|
|
foreach ($this->temp_files as $pattern) { |
371
|
|
|
if (preg_match($pattern, $this->name)) { |
372
|
|
|
return true; |
373
|
|
|
} |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
return false; |
377
|
|
|
} |
378
|
|
|
|
379
|
|
|
/** |
380
|
|
|
* Delete version. |
381
|
|
|
* |
382
|
|
|
* @param int $version |
383
|
|
|
* |
384
|
|
|
* @return bool |
385
|
|
|
*/ |
386
|
|
|
public function deleteVersion(int $version): bool |
387
|
|
|
{ |
388
|
|
|
$key = array_search($version, array_column($this->history, 'version'), true); |
389
|
|
|
|
390
|
|
|
if (false === $key) { |
391
|
|
|
throw new Exception('version '.$version.' does not exists'); |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
try { |
395
|
|
|
if ($this->history[$key]['storage'] !== null) { |
396
|
|
|
$this->_storage->deleteFile($this, $this->history[$key]['storage']); |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
array_splice($this->history, $key, 1); |
400
|
|
|
|
401
|
|
|
$this->_logger->debug('removed version ['.$version.'] from file ['.$this->_id.']', [ |
402
|
|
|
'category' => get_class($this), |
403
|
|
|
]); |
404
|
|
|
|
405
|
|
|
return $this->save('history'); |
406
|
|
|
} catch (\Exception $e) { |
407
|
|
|
$this->_logger->error('failed remove version ['.$version.'] from file ['.$this->_id.']', [ |
408
|
|
|
'category' => get_class($this), |
409
|
|
|
'exception' => $e, |
410
|
|
|
]); |
411
|
|
|
|
412
|
|
|
throw $e; |
413
|
|
|
} |
414
|
|
|
} |
415
|
|
|
|
416
|
|
|
/** |
417
|
|
|
* Cleanup history. |
418
|
|
|
* |
419
|
|
|
* @return bool |
420
|
|
|
*/ |
421
|
|
|
public function cleanHistory(): bool |
422
|
|
|
{ |
423
|
|
|
foreach ($this->history as $node) { |
424
|
|
|
$this->deleteVersion($node['version']); |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
return true; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* Get Attributes. |
432
|
|
|
* |
433
|
|
|
* @return array |
434
|
|
|
*/ |
435
|
|
|
public function getAttributes(): array |
436
|
|
|
{ |
437
|
|
|
return [ |
438
|
|
|
'_id' => $this->_id, |
439
|
|
|
'name' => $this->name, |
440
|
|
|
'hash' => $this->hash, |
441
|
|
|
'directory' => false, |
442
|
|
|
'size' => $this->size, |
443
|
|
|
'version' => $this->version, |
444
|
|
|
'parent' => $this->parent, |
445
|
|
|
'acl' => $this->acl, |
446
|
|
|
'app' => $this->app, |
447
|
|
|
'meta' => $this->meta, |
448
|
|
|
'mime' => $this->mime, |
449
|
|
|
'owner' => $this->owner, |
450
|
|
|
'history' => $this->history, |
451
|
|
|
'shared' => $this->shared, |
452
|
|
|
'deleted' => $this->deleted, |
453
|
|
|
'changed' => $this->changed, |
454
|
|
|
'created' => $this->created, |
455
|
|
|
'destroy' => $this->destroy, |
456
|
|
|
'readonly' => $this->readonly, |
457
|
|
|
'storage_adapter' => $this->storage_adapter, |
458
|
|
|
'storage' => $this->storage, |
459
|
|
|
]; |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
/** |
463
|
|
|
* Get filename extension. |
464
|
|
|
* |
465
|
|
|
* @return string |
466
|
|
|
*/ |
467
|
|
|
public function getExtension(): string |
468
|
|
|
{ |
469
|
|
|
$ext = strrchr($this->name, '.'); |
470
|
|
|
if (false === $ext) { |
471
|
|
|
throw new Exception('file does not have an extension'); |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
return substr($ext, 1); |
475
|
|
|
} |
476
|
|
|
|
477
|
|
|
/** |
478
|
|
|
* Get file size. |
479
|
|
|
* |
480
|
|
|
* @return int |
481
|
|
|
*/ |
482
|
|
|
public function getSize(): int |
483
|
|
|
{ |
484
|
|
|
return $this->size; |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
/** |
488
|
|
|
* Get md5 sum of the file content, |
489
|
|
|
* actually the hash value comes from the database. |
490
|
|
|
* |
491
|
|
|
* @return string |
492
|
|
|
*/ |
493
|
|
|
public function getETag(): string |
494
|
|
|
{ |
495
|
|
|
return "'".$this->hash."'"; |
496
|
|
|
} |
497
|
|
|
|
498
|
|
|
/** |
499
|
|
|
* Get hash. |
500
|
|
|
* |
501
|
|
|
* @return string |
502
|
|
|
*/ |
503
|
|
|
public function getHash(): ?string |
504
|
|
|
{ |
505
|
|
|
return $this->hash; |
506
|
|
|
} |
507
|
|
|
|
508
|
|
|
/** |
509
|
|
|
* Get version. |
510
|
|
|
* |
511
|
|
|
* @return int |
512
|
|
|
*/ |
513
|
|
|
public function getVersion(): int |
514
|
|
|
{ |
515
|
|
|
return $this->version; |
516
|
|
|
} |
517
|
|
|
|
518
|
|
|
/** |
519
|
|
|
* Change content. |
520
|
|
|
* |
521
|
|
|
* @param resource|string $file |
522
|
|
|
* @param bool $new |
523
|
|
|
* @param array $attributes |
524
|
|
|
* |
525
|
|
|
* @return int |
526
|
|
|
*/ |
527
|
|
|
public function put($file, bool $new = false, array $attributes = []): int |
528
|
|
|
{ |
529
|
|
|
$this->_logger->debug('add contents for file ['.$this->_id.']', [ |
530
|
|
|
'category' => get_class($this), |
531
|
|
|
]); |
532
|
|
|
|
533
|
|
|
$this->validatePutRequest($file, $new, $attributes); |
534
|
|
|
$file = $this->createTemporaryFile($file, $stream); |
535
|
|
|
$new_hash = $this->verifyFile($file, $new); |
536
|
|
|
|
537
|
|
|
if ($this->hash === $new_hash) { |
538
|
|
|
$this->_logger->info('stop PUT execution, content checksums are equal for file ['.$this->_id.']', [ |
539
|
|
|
'category' => get_class($this), |
540
|
|
|
]); |
541
|
|
|
|
542
|
|
|
//Remove tmp file |
543
|
|
|
if (null !== $file) { |
544
|
|
|
unlink($file); |
545
|
|
|
fclose($stream); |
546
|
|
|
} |
547
|
|
|
|
548
|
|
|
return $this->version; |
549
|
|
|
} |
550
|
|
|
|
551
|
|
|
$this->hash = $new_hash; |
552
|
|
|
$max = (int) (string) $this->_fs->getServer()->getMaxFileVersion(); |
553
|
|
|
if (count($this->history) >= $max) { |
554
|
|
|
$del = key($this->history); |
555
|
|
|
$this->_logger->debug('history limit ['.$max.'] reached, remove oldest version ['.$del.'] from file ['.$this->_id.']', [ |
556
|
|
|
'category' => get_class($this), |
557
|
|
|
]); |
558
|
|
|
|
559
|
|
|
$this->deleteVersion($this->history[$del]['version']); |
560
|
|
|
} |
561
|
|
|
|
562
|
|
|
//Write new content |
563
|
|
|
if ($this->size > 0) { |
564
|
|
|
$this->storage = $this->_storage->storeFile($this, $stream, $this->storage_adapter); |
565
|
|
|
} else { |
566
|
|
|
$this->storage = null; |
567
|
|
|
} |
568
|
|
|
|
569
|
|
|
//Update current version |
570
|
|
|
$this->increaseVersion(); |
571
|
|
|
|
572
|
|
|
//Get meta attributes |
573
|
|
|
if (isset($attributes['mime'])) { |
574
|
|
|
$this->mime = $attributes['mime']; |
575
|
|
|
} elseif (null !== $file) { |
576
|
|
|
$this->mime = (new Mime())->getMime($file, $this->name); |
577
|
|
|
} |
578
|
|
|
|
579
|
|
|
//Remove tmp file |
580
|
|
|
if (null !== $file) { |
581
|
|
|
unlink($file); |
582
|
|
|
fclose($stream); |
583
|
|
|
} |
584
|
|
|
|
585
|
|
|
$this->_logger->debug('set mime ['.$this->mime.'] for content, file=['.$this->_id.']', [ |
586
|
|
|
'category' => get_class($this), |
587
|
|
|
]); |
588
|
|
|
|
589
|
|
|
$this->addVersion($attributes) |
590
|
|
|
->postPutFile($file, $new, $attributes); |
591
|
|
|
|
592
|
|
|
return $this->version; |
593
|
|
|
} |
594
|
|
|
|
595
|
|
|
/** |
596
|
|
|
* Completly remove file. |
597
|
|
|
* |
598
|
|
|
* @return bool |
599
|
|
|
*/ |
600
|
|
|
protected function _forceDelete(): bool |
601
|
|
|
{ |
602
|
|
|
try { |
603
|
|
|
$this->cleanHistory(); |
604
|
|
|
$this->_db->storage->deleteOne([ |
605
|
|
|
'_id' => $this->_id, |
606
|
|
|
]); |
607
|
|
|
|
608
|
|
|
$this->_logger->info('removed file node ['.$this->_id.']', [ |
609
|
|
|
'category' => get_class($this), |
610
|
|
|
]); |
611
|
|
|
} catch (\Exception $e) { |
612
|
|
|
$this->_logger->error('failed delete file node ['.$this->_id.']', [ |
613
|
|
|
'category' => get_class($this), |
614
|
|
|
'exception' => $e, |
615
|
|
|
]); |
616
|
|
|
|
617
|
|
|
throw $e; |
618
|
|
|
} |
619
|
|
|
|
620
|
|
|
return true; |
621
|
|
|
} |
622
|
|
|
|
623
|
|
|
/** |
624
|
|
|
* Increase version. |
625
|
|
|
* |
626
|
|
|
* @return int |
627
|
|
|
*/ |
628
|
|
|
protected function increaseVersion(): int |
629
|
|
|
{ |
630
|
|
|
++$this->version; |
631
|
|
|
|
632
|
|
|
return $this->version; |
633
|
|
|
} |
634
|
|
|
|
635
|
|
|
/** |
636
|
|
|
* Create uuidv4. |
637
|
|
|
* |
638
|
|
|
* @param string $data |
639
|
|
|
* |
640
|
|
|
* @return string |
641
|
|
|
*/ |
642
|
|
|
protected function guidv4(string $data): string |
643
|
|
|
{ |
644
|
|
|
assert(16 === strlen($data)); |
645
|
|
|
|
646
|
|
|
$data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100 |
647
|
|
|
$data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10 |
648
|
|
|
|
649
|
|
|
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); |
650
|
|
|
} |
651
|
|
|
|
652
|
|
|
/** |
653
|
|
|
* Change content. |
654
|
|
|
* |
655
|
|
|
* @param resource|string $file |
656
|
|
|
* @param bool $new |
657
|
|
|
* @param array $attributes |
658
|
|
|
* |
659
|
|
|
* @return bool |
660
|
|
|
*/ |
661
|
|
|
protected function validatePutRequest($file, bool $new = false, array $attributes = []): bool |
662
|
|
|
{ |
663
|
|
|
if (!$this->_acl->isAllowed($this, 'w')) { |
664
|
|
|
throw new ForbiddenException( |
665
|
|
|
'not allowed to modify node', |
666
|
|
|
ForbiddenException::NOT_ALLOWED_TO_MODIFY |
667
|
|
|
); |
668
|
|
|
} |
669
|
|
|
|
670
|
|
|
$this->_hook->run('prePutFile', [$this, &$file, &$new, &$attributes]); |
671
|
|
|
|
672
|
|
|
if ($this->readonly) { |
673
|
|
|
throw new Exception\Conflict( |
674
|
|
|
'node is marked as readonly, it is not possible to change any content', |
675
|
|
|
Exception\Conflict::READONLY |
676
|
|
|
); |
677
|
|
|
} |
678
|
|
|
|
679
|
|
|
if ($this->isShareMember() && false === $new && 'w' === $this->_acl->getAclPrivilege($this->getShareNode())) { |
|
|
|
|
680
|
|
|
throw new ForbiddenException( |
681
|
|
|
'not allowed to overwrite node', |
682
|
|
|
ForbiddenException::NOT_ALLOWED_TO_OVERWRITE |
683
|
|
|
); |
684
|
|
|
} |
685
|
|
|
|
686
|
|
|
return true; |
687
|
|
|
} |
688
|
|
|
|
689
|
|
|
/** |
690
|
|
|
* Verify content to be added. |
691
|
|
|
* |
692
|
|
|
* @param string $path |
693
|
|
|
* @param bool $new |
694
|
|
|
* |
695
|
|
|
* @return bool |
696
|
|
|
*/ |
697
|
|
|
protected function verifyFile(?string $path, bool $new = false): string |
698
|
|
|
{ |
699
|
|
|
if (null === $path) { |
700
|
|
|
$this->size = 0; |
701
|
|
|
$new_hash = self::EMPTY_CONTENT; |
702
|
|
|
} else { |
703
|
|
|
$size = filesize($path); |
704
|
|
|
$this->size = $size; |
705
|
|
|
$new_hash = md5_file($path); |
706
|
|
|
|
707
|
|
|
if (!$this->_user->checkQuota($size)) { |
708
|
|
|
$this->_logger->warning('could not execute PUT, user quota is full', [ |
709
|
|
|
'category' => get_class($this), |
710
|
|
|
]); |
711
|
|
|
|
712
|
|
|
if (true === $new) { |
713
|
|
|
$this->_forceDelete(); |
714
|
|
|
} |
715
|
|
|
|
716
|
|
|
throw new Exception\InsufficientStorage( |
717
|
|
|
'user quota is full', |
718
|
|
|
Exception\InsufficientStorage::USER_QUOTA_FULL |
719
|
|
|
); |
720
|
|
|
} |
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
return $new_hash; |
724
|
|
|
} |
725
|
|
|
|
726
|
|
|
/** |
727
|
|
|
* Create temporary file. |
728
|
|
|
* |
729
|
|
|
* @param resource|string $file |
730
|
|
|
* @param resource $stream |
731
|
|
|
* |
732
|
|
|
* @return string |
733
|
|
|
*/ |
734
|
|
|
protected function createTemporaryFile($file, &$stream): ?string |
735
|
|
|
{ |
736
|
|
|
if (is_string($file)) { |
737
|
|
|
if (!is_readable($file)) { |
738
|
|
|
throw new Exception('file does not exists or is not readable'); |
739
|
|
|
} |
740
|
|
|
|
741
|
|
|
$stream = fopen($file, 'r'); |
742
|
|
|
} elseif (is_resource($file)) { |
743
|
|
|
$tmp = $this->_fs->getServer()->getTempDir().DIRECTORY_SEPARATOR.'upload'.DIRECTORY_SEPARATOR.$this->_user->getId(); |
744
|
|
|
if (!file_exists($tmp)) { |
745
|
|
|
mkdir($tmp, 0700, true); |
746
|
|
|
} |
747
|
|
|
|
748
|
|
|
$tmp_file = $tmp.DIRECTORY_SEPARATOR.$this->guidv4(openssl_random_pseudo_bytes(16)); |
749
|
|
|
$stream = fopen($tmp_file, 'w+'); |
750
|
|
|
$size = stream_copy_to_stream($file, $stream, ((int) $this->_fs->getServer()->getMaxFileSize() + 1)); |
751
|
|
|
rewind($stream); |
752
|
|
|
fclose($file); |
753
|
|
|
|
754
|
|
|
if ($size > (int) $this->_fs->getServer()->getMaxFileSize()) { |
755
|
|
|
unlink($tmp_file); |
756
|
|
|
|
757
|
|
|
throw new Exception\InsufficientStorage( |
758
|
|
|
'file size exceeded limit', |
759
|
|
|
Exception\InsufficientStorage::FILE_SIZE_LIMIT |
760
|
|
|
); |
761
|
|
|
} |
762
|
|
|
|
763
|
|
|
$file = $tmp_file; |
764
|
|
|
} else { |
765
|
|
|
$file = null; |
766
|
|
|
} |
767
|
|
|
|
768
|
|
|
return $file; |
769
|
|
|
} |
770
|
|
|
|
771
|
|
|
/** |
772
|
|
|
* Add new version. |
773
|
|
|
* |
774
|
|
|
* @param array $attributes |
775
|
|
|
* |
776
|
|
|
* @return File |
777
|
|
|
*/ |
778
|
|
|
protected function addVersion(array $attributes = []): self |
779
|
|
|
{ |
780
|
|
|
if (1 !== $this->version) { |
781
|
|
|
if (isset($attributes['changed'])) { |
782
|
|
|
if (!($attributes['changed'] instanceof UTCDateTime)) { |
|
|
|
|
783
|
|
|
throw new Exception\InvalidArgument('attribute changed must be an instance of UTCDateTime'); |
784
|
|
|
} |
785
|
|
|
|
786
|
|
|
$this->changed = $attributes['changed']; |
787
|
|
|
} else { |
788
|
|
|
$this->changed = new UTCDateTime(); |
789
|
|
|
} |
790
|
|
|
|
791
|
|
|
$this->_logger->debug('added new history version ['.$this->version.'] for file ['.$this->_id.']', [ |
792
|
|
|
'category' => get_class($this), |
793
|
|
|
]); |
794
|
|
|
|
795
|
|
|
$this->history[] = [ |
796
|
|
|
'version' => $this->version, |
797
|
|
|
'changed' => $this->changed, |
798
|
|
|
'user' => $this->_user->getId(), |
799
|
|
|
'type' => self::HISTORY_EDIT, |
800
|
|
|
'storage' => $this->storage, |
801
|
|
|
'storage_adapter' => $this->storage_adapter, |
802
|
|
|
'size' => $this->size, |
803
|
|
|
'mime' => $this->mime, |
804
|
|
|
'hash' => $this->hash, |
805
|
|
|
]; |
806
|
|
|
} else { |
807
|
|
|
$this->_logger->debug('added first file version [1] for file ['.$this->_id.']', [ |
808
|
|
|
'category' => get_class($this), |
809
|
|
|
]); |
810
|
|
|
|
811
|
|
|
$this->history[0] = [ |
812
|
|
|
'version' => 1, |
813
|
|
|
'changed' => isset($attributes['changed']) ? $attributes['changed'] : new UTCDateTime(), |
814
|
|
|
'user' => $this->owner, |
815
|
|
|
'type' => self::HISTORY_CREATE, |
816
|
|
|
'storage' => $this->storage, |
817
|
|
|
'storage_adapter' => $this->storage_adapter, |
818
|
|
|
'size' => $this->size, |
819
|
|
|
'mime' => $this->mime, |
820
|
|
|
'hash' => $this->hash, |
821
|
|
|
]; |
822
|
|
|
} |
823
|
|
|
|
824
|
|
|
return $this; |
825
|
|
|
} |
826
|
|
|
|
827
|
|
|
/** |
828
|
|
|
* Finalize put request. |
829
|
|
|
* |
830
|
|
|
* @param resource|string $file |
831
|
|
|
* @param bool $new |
832
|
|
|
* @param array $attributes |
833
|
|
|
* |
834
|
|
|
* @return File |
835
|
|
|
*/ |
836
|
|
|
protected function postPutFile($file, bool $new, array $attributes): self |
837
|
|
|
{ |
838
|
|
|
try { |
839
|
|
|
$this->save([ |
840
|
|
|
'size', |
841
|
|
|
'changed', |
842
|
|
|
'mime', |
843
|
|
|
'hash', |
844
|
|
|
'version', |
845
|
|
|
'history', |
846
|
|
|
'storage', |
847
|
|
|
'storage_adapter', |
848
|
|
|
]); |
849
|
|
|
|
850
|
|
|
$this->_logger->debug('modifed file metadata ['.$this->_id.']', [ |
851
|
|
|
'category' => get_class($this), |
852
|
|
|
]); |
853
|
|
|
|
854
|
|
|
$this->_hook->run('postPutFile', [$this, $file, $new, $attributes]); |
855
|
|
|
|
856
|
|
|
return $this; |
857
|
|
|
} catch (\Exception $e) { |
858
|
|
|
$this->_logger->error('failed modify file metadata ['.$this->_id.']', [ |
859
|
|
|
'category' => get_class($this), |
860
|
|
|
'exception' => $e, |
861
|
|
|
]); |
862
|
|
|
|
863
|
|
|
throw $e; |
864
|
|
|
} |
865
|
|
|
} |
866
|
|
|
} |
867
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.