Passed
Push — master ( 71f698...5e7187 )
by John
22:15 queued 09:06
created

ObjectStoreStorage   F

Complexity

Total Complexity 108

Size/Duplication

Total Lines 574
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 316
c 1
b 0
f 0
dl 0
loc 574
rs 2
wmc 108

28 Methods

Rating   Name   Duplication   Size   Complexity  
A getObjectStore() 0 2 1
A getMimeType() 0 3 1
B mkdir() 0 40 6
A stat() 0 7 2
B __construct() 0 23 7
A getScanner() 0 8 3
A getId() 0 2 1
A touch() 0 41 5
A normalizePath() 0 11 3
A unlink() 0 24 6
D fopen() 0 68 22
A rmObjects() 0 15 5
A getURN() 0 5 2
A copy() 0 13 2
A copyFile() 0 19 3
A filetype() 0 10 3
A needsPartFile() 0 2 1
A opendir() 0 14 3
A rename() 0 7 1
C writeStream() 0 83 12
A file_exists() 0 3 1
A copyFromStorage() 0 19 5
A file_put_contents() 0 5 1
A copyInner() 0 14 4
A writeBack() 0 3 1
A getPermissions() 0 8 3
A rmdir() 0 14 3
A hasUpdated() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like ObjectStoreStorage often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ObjectStoreStorage, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Bjoern Schiessle <[email protected]>
6
 * @author Christoph Wurst <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author Jörn Friedrich Dreyer <[email protected]>
9
 * @author Marcel Klehr <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Robin Appelman <[email protected]>
12
 * @author Roeland Jago Douma <[email protected]>
13
 * @author Tigran Mkrtchyan <[email protected]>
14
 *
15
 * @license AGPL-3.0
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program. If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
namespace OC\Files\ObjectStore;
31
32
use Icewind\Streams\CallbackWrapper;
33
use Icewind\Streams\CountWrapper;
34
use Icewind\Streams\IteratorDirectory;
35
use OC\Files\Cache\Cache;
36
use OC\Files\Cache\CacheEntry;
37
use OC\Files\Storage\PolyFill\CopyDirectory;
38
use OCP\Files\Cache\ICacheEntry;
39
use OCP\Files\FileInfo;
40
use OCP\Files\NotFoundException;
41
use OCP\Files\ObjectStore\IObjectStore;
42
use OCP\Files\Storage\IStorage;
43
44
class ObjectStoreStorage extends \OC\Files\Storage\Common {
45
	use CopyDirectory;
46
47
	/**
48
	 * @var \OCP\Files\ObjectStore\IObjectStore $objectStore
49
	 */
50
	protected $objectStore;
51
	/**
52
	 * @var string $id
53
	 */
54
	protected $id;
55
	/**
56
	 * @var \OC\User\User $user
57
	 */
58
	protected $user;
59
60
	private $objectPrefix = 'urn:oid:';
61
62
	private $logger;
63
64
	/** @var bool */
65
	protected $validateWrites = true;
66
67
	public function __construct($params) {
68
		if (isset($params['objectstore']) && $params['objectstore'] instanceof IObjectStore) {
69
			$this->objectStore = $params['objectstore'];
70
		} else {
71
			throw new \Exception('missing IObjectStore instance');
72
		}
73
		if (isset($params['storageid'])) {
74
			$this->id = 'object::store:' . $params['storageid'];
75
		} else {
76
			$this->id = 'object::store:' . $this->objectStore->getStorageId();
77
		}
78
		if (isset($params['objectPrefix'])) {
79
			$this->objectPrefix = $params['objectPrefix'];
80
		}
81
		if (isset($params['validateWrites'])) {
82
			$this->validateWrites = (bool)$params['validateWrites'];
83
		}
84
		//initialize cache with root directory in cache
85
		if (!$this->is_dir('/')) {
86
			$this->mkdir('/');
87
		}
88
89
		$this->logger = \OC::$server->getLogger();
90
	}
91
92
	public function mkdir($path) {
93
		$path = $this->normalizePath($path);
94
95
		if ($this->file_exists($path)) {
96
			return false;
97
		}
98
99
		$mTime = time();
100
		$data = [
101
			'mimetype' => 'httpd/unix-directory',
102
			'size' => 0,
103
			'mtime' => $mTime,
104
			'storage_mtime' => $mTime,
105
			'permissions' => \OCP\Constants::PERMISSION_ALL,
106
		];
107
		if ($path === '') {
108
			//create root on the fly
109
			$data['etag'] = $this->getETag('');
110
			$this->getCache()->put('', $data);
111
			return true;
112
		} else {
113
			// if parent does not exist, create it
114
			$parent = $this->normalizePath(dirname($path));
115
			$parentType = $this->filetype($parent);
116
			if ($parentType === false) {
117
				if (!$this->mkdir($parent)) {
118
					// something went wrong
119
					return false;
120
				}
121
			} elseif ($parentType === 'file') {
122
				// parent is a file
123
				return false;
124
			}
125
			// finally create the new dir
126
			$mTime = time(); // update mtime
127
			$data['mtime'] = $mTime;
128
			$data['storage_mtime'] = $mTime;
129
			$data['etag'] = $this->getETag($path);
130
			$this->getCache()->put($path, $data);
131
			return true;
132
		}
133
	}
134
135
	/**
136
	 * @param string $path
137
	 * @return string
138
	 */
139
	private function normalizePath($path) {
140
		$path = trim($path, '/');
141
		//FIXME why do we sometimes get a path like 'files//username'?
142
		$path = str_replace('//', '/', $path);
143
144
		// dirname('/folder') returns '.' but internally (in the cache) we store the root as ''
145
		if (!$path || $path === '.') {
146
			$path = '';
147
		}
148
149
		return $path;
150
	}
151
152
	/**
153
	 * Object Stores use a NoopScanner because metadata is directly stored in
154
	 * the file cache and cannot really scan the filesystem. The storage passed in is not used anywhere.
155
	 *
156
	 * @param string $path
157
	 * @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner
0 ignored issues
show
Bug introduced by
The type OC\Files\ObjectStore\optional was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
158
	 * @return \OC\Files\ObjectStore\NoopScanner
159
	 */
160
	public function getScanner($path = '', $storage = null) {
161
		if (!$storage) {
162
			$storage = $this;
163
		}
164
		if (!isset($this->scanner)) {
165
			$this->scanner = new NoopScanner($storage);
166
		}
167
		return $this->scanner;
168
	}
169
170
	public function getId() {
171
		return $this->id;
172
	}
173
174
	public function rmdir($path) {
175
		$path = $this->normalizePath($path);
176
177
		if (!$this->is_dir($path)) {
178
			return false;
179
		}
180
181
		if (!$this->rmObjects($path)) {
182
			return false;
183
		}
184
185
		$this->getCache()->remove($path);
186
187
		return true;
188
	}
189
190
	private function rmObjects($path) {
191
		$children = $this->getCache()->getFolderContents($path);
192
		foreach ($children as $child) {
193
			if ($child['mimetype'] === 'httpd/unix-directory') {
194
				if (!$this->rmObjects($child['path'])) {
195
					return false;
196
				}
197
			} else {
198
				if (!$this->unlink($child['path'])) {
199
					return false;
200
				}
201
			}
202
		}
203
204
		return true;
205
	}
206
207
	public function unlink($path) {
208
		$path = $this->normalizePath($path);
209
		$stat = $this->stat($path);
210
211
		if ($stat && isset($stat['fileid'])) {
212
			if ($stat['mimetype'] === 'httpd/unix-directory') {
213
				return $this->rmdir($path);
214
			}
215
			try {
216
				$this->objectStore->deleteObject($this->getURN($stat['fileid']));
217
			} catch (\Exception $ex) {
218
				if ($ex->getCode() !== 404) {
219
					$this->logger->logException($ex, [
220
						'app' => 'objectstore',
221
						'message' => 'Could not delete object ' . $this->getURN($stat['fileid']) . ' for ' . $path,
222
					]);
223
					return false;
224
				}
225
				//removing from cache is ok as it does not exist in the objectstore anyway
226
			}
227
			$this->getCache()->remove($path);
228
			return true;
229
		}
230
		return false;
231
	}
232
233
	public function stat($path) {
234
		$path = $this->normalizePath($path);
235
		$cacheEntry = $this->getCache()->get($path);
236
		if ($cacheEntry instanceof CacheEntry) {
237
			return $cacheEntry->getData();
238
		} else {
239
			return false;
240
		}
241
	}
242
243
	public function getPermissions($path) {
244
		$stat = $this->stat($path);
245
246
		if (is_array($stat) && isset($stat['permissions'])) {
247
			return $stat['permissions'];
248
		}
249
250
		return parent::getPermissions($path);
251
	}
252
253
	/**
254
	 * Override this method if you need a different unique resource identifier for your object storage implementation.
255
	 * The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users.
256
	 * You may need a mapping table to store your URN if it cannot be generated from the fileid.
257
	 *
258
	 * @param int $fileId the fileid
259
	 * @return null|string the unified resource name used to identify the object
260
	 */
261
	public function getURN($fileId) {
262
		if (is_numeric($fileId)) {
0 ignored issues
show
introduced by
The condition is_numeric($fileId) is always true.
Loading history...
263
			return $this->objectPrefix . $fileId;
264
		}
265
		return null;
266
	}
267
268
	public function opendir($path) {
269
		$path = $this->normalizePath($path);
270
271
		try {
272
			$files = [];
273
			$folderContents = $this->getCache()->getFolderContents($path);
274
			foreach ($folderContents as $file) {
275
				$files[] = $file['name'];
276
			}
277
278
			return IteratorDirectory::wrap($files);
279
		} catch (\Exception $e) {
280
			$this->logger->logException($e);
281
			return false;
282
		}
283
	}
284
285
	public function filetype($path) {
286
		$path = $this->normalizePath($path);
287
		$stat = $this->stat($path);
288
		if ($stat) {
289
			if ($stat['mimetype'] === 'httpd/unix-directory') {
290
				return 'dir';
291
			}
292
			return 'file';
293
		} else {
294
			return false;
295
		}
296
	}
297
298
	public function fopen($path, $mode) {
299
		$path = $this->normalizePath($path);
300
301
		if (strrpos($path, '.') !== false) {
302
			$ext = substr($path, strrpos($path, '.'));
303
		} else {
304
			$ext = '';
305
		}
306
307
		switch ($mode) {
308
			case 'r':
309
			case 'rb':
310
				$stat = $this->stat($path);
311
				if (is_array($stat)) {
312
					// Reading 0 sized files is a waste of time
313
					if (isset($stat['size']) && $stat['size'] === 0) {
314
						return fopen('php://memory', $mode);
315
					}
316
317
					try {
318
						return $this->objectStore->readObject($this->getURN($stat['fileid']));
319
					} catch (NotFoundException $e) {
320
						$this->logger->logException($e, [
321
							'app' => 'objectstore',
322
							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
323
						]);
324
						throw $e;
325
					} catch (\Exception $ex) {
326
						$this->logger->logException($ex, [
327
							'app' => 'objectstore',
328
							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
329
						]);
330
						return false;
331
					}
332
				} else {
333
					return false;
334
				}
335
			// no break
336
			case 'w':
337
			case 'wb':
338
			case 'w+':
339
			case 'wb+':
340
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
341
				$handle = fopen($tmpFile, $mode);
342
				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
343
					$this->writeBack($tmpFile, $path);
344
					unlink($tmpFile);
345
				});
346
			case 'a':
347
			case 'ab':
348
			case 'r+':
349
			case 'a+':
350
			case 'x':
351
			case 'x+':
352
			case 'c':
353
			case 'c+':
354
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
355
				if ($this->file_exists($path)) {
356
					$source = $this->fopen($path, 'r');
357
					file_put_contents($tmpFile, $source);
358
				}
359
				$handle = fopen($tmpFile, $mode);
360
				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
361
					$this->writeBack($tmpFile, $path);
362
					unlink($tmpFile);
363
				});
364
		}
365
		return false;
366
	}
367
368
	public function file_exists($path) {
369
		$path = $this->normalizePath($path);
370
		return (bool)$this->stat($path);
371
	}
372
373
	public function rename($source, $target) {
374
		$source = $this->normalizePath($source);
375
		$target = $this->normalizePath($target);
376
		$this->remove($target);
377
		$this->getCache()->move($source, $target);
378
		$this->touch(dirname($target));
379
		return true;
380
	}
381
382
	public function getMimeType($path) {
383
		$path = $this->normalizePath($path);
384
		return parent::getMimeType($path);
385
	}
386
387
	public function touch($path, $mtime = null) {
388
		if (is_null($mtime)) {
389
			$mtime = time();
390
		}
391
392
		$path = $this->normalizePath($path);
393
		$dirName = dirname($path);
394
		$parentExists = $this->is_dir($dirName);
395
		if (!$parentExists) {
396
			return false;
397
		}
398
399
		$stat = $this->stat($path);
400
		if (is_array($stat)) {
401
			// update existing mtime in db
402
			$stat['mtime'] = $mtime;
403
			$this->getCache()->update($stat['fileid'], $stat);
404
		} else {
405
			try {
406
				//create a empty file, need to have at least on char to make it
407
				// work with all object storage implementations
408
				$this->file_put_contents($path, ' ');
409
				$mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
410
				$stat = [
411
					'etag' => $this->getETag($path),
412
					'mimetype' => $mimeType,
413
					'size' => 0,
414
					'mtime' => $mtime,
415
					'storage_mtime' => $mtime,
416
					'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
417
				];
418
				$this->getCache()->put($path, $stat);
419
			} catch (\Exception $ex) {
420
				$this->logger->logException($ex, [
421
					'app' => 'objectstore',
422
					'message' => 'Could not create object for ' . $path,
423
				]);
424
				throw $ex;
425
			}
426
		}
427
		return true;
428
	}
429
430
	public function writeBack($tmpFile, $path) {
431
		$size = filesize($tmpFile);
432
		$this->writeStream($path, fopen($tmpFile, 'r'), $size);
433
	}
434
435
	/**
436
	 * external changes are not supported, exclusive access to the object storage is assumed
437
	 *
438
	 * @param string $path
439
	 * @param int $time
440
	 * @return false
441
	 */
442
	public function hasUpdated($path, $time) {
443
		return false;
444
	}
445
446
	public function needsPartFile() {
447
		return false;
448
	}
449
450
	public function file_put_contents($path, $data) {
451
		$handle = $this->fopen($path, 'w+');
452
		$result = fwrite($handle, $data);
453
		fclose($handle);
454
		return $result;
455
	}
456
457
	public function writeStream(string $path, $stream, int $size = null): int {
458
		$stat = $this->stat($path);
459
		if (empty($stat)) {
460
			// create new file
461
			$stat = [
462
				'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
463
			];
464
		}
465
		// update stat with new data
466
		$mTime = time();
467
		$stat['size'] = (int)$size;
468
		$stat['mtime'] = $mTime;
469
		$stat['storage_mtime'] = $mTime;
470
471
		$mimetypeDetector = \OC::$server->getMimeTypeDetector();
472
		$mimetype = $mimetypeDetector->detectPath($path);
473
474
		$stat['mimetype'] = $mimetype;
475
		$stat['etag'] = $this->getETag($path);
476
		$stat['checksum'] = '';
477
478
		$exists = $this->getCache()->inCache($path);
479
		$uploadPath = $exists ? $path : $path . '.part';
480
481
		if ($exists) {
482
			$fileId = $stat['fileid'];
483
		} else {
484
			$fileId = $this->getCache()->put($uploadPath, $stat);
485
		}
486
487
		$urn = $this->getURN($fileId);
488
		try {
489
			//upload to object storage
490
			if ($size === null) {
491
				$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
492
					$this->getCache()->update($fileId, [
493
						'size' => $writtenSize,
494
					]);
495
					$size = $writtenSize;
496
				});
497
				$this->objectStore->writeObject($urn, $countStream, $mimetype);
498
				if (is_resource($countStream)) {
499
					fclose($countStream);
500
				}
501
				$stat['size'] = $size;
502
			} else {
503
				$this->objectStore->writeObject($urn, $stream, $mimetype);
504
				if (is_resource($stream)) {
505
					fclose($stream);
506
				}
507
			}
508
		} catch (\Exception $ex) {
509
			if (!$exists) {
510
				/*
511
				 * Only remove the entry if we are dealing with a new file.
512
				 * Else people lose access to existing files
513
				 */
514
				$this->getCache()->remove($uploadPath);
515
				$this->logger->logException($ex, [
516
					'app' => 'objectstore',
517
					'message' => 'Could not create object ' . $urn . ' for ' . $path,
518
				]);
519
			} else {
520
				$this->logger->logException($ex, [
521
					'app' => 'objectstore',
522
					'message' => 'Could not update object ' . $urn . ' for ' . $path,
523
				]);
524
			}
525
			throw $ex; // make this bubble up
526
		}
527
528
		if ($exists) {
529
			$this->getCache()->update($fileId, $stat);
530
		} else {
531
			if (!$this->validateWrites || $this->objectStore->objectExists($urn)) {
532
				$this->getCache()->move($uploadPath, $path);
533
			} else {
534
				$this->getCache()->remove($uploadPath);
535
				throw new \Exception("Object not found after writing (urn: $urn, path: $path)", 404);
536
			}
537
		}
538
539
		return $size;
540
	}
541
542
	public function getObjectStore(): IObjectStore {
543
		return $this->objectStore;
544
	}
545
546
	public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
547
		if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
548
			/** @var ObjectStoreStorage $sourceStorage */
549
			if ($sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()) {
550
				/** @var CacheEntry $sourceEntry */
551
				$sourceEntry = $sourceStorage->getCache()->get($sourceInternalPath);
552
				$sourceEntryData = $sourceEntry->getData();
553
				// $sourceEntry['permissions'] here is the permissions from the jailed storage for the current
554
				// user. Instead we use $sourceEntryData['scan_permissions'] that are the permissions from the
555
				// unjailed storage.
556
				if (is_array($sourceEntryData) && array_key_exists('scan_permissions', $sourceEntryData)) {
557
					$sourceEntry['permissions'] = $sourceEntryData['scan_permissions'];
558
				}
559
				$this->copyInner($sourceEntry, $targetInternalPath);
560
				return true;
561
			}
562
		}
563
564
		return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
565
	}
566
567
	public function copy($source, $target) {
568
		$source = $this->normalizePath($source);
569
		$target = $this->normalizePath($target);
570
571
		$cache = $this->getCache();
572
		$sourceEntry = $cache->get($source);
573
		if (!$sourceEntry) {
574
			throw new NotFoundException('Source object not found');
575
		}
576
577
		$this->copyInner($sourceEntry, $target);
578
579
		return true;
580
	}
581
582
	private function copyInner(ICacheEntry $sourceEntry, string $to) {
583
		$cache = $this->getCache();
584
585
		if ($sourceEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
586
			if ($cache->inCache($to)) {
587
				$cache->remove($to);
588
			}
589
			$this->mkdir($to);
590
591
			foreach ($cache->getFolderContentsById($sourceEntry->getId()) as $child) {
592
				$this->copyInner($child, $to . '/' . $child->getName());
593
			}
594
		} else {
595
			$this->copyFile($sourceEntry, $to);
596
		}
597
	}
598
599
	private function copyFile(ICacheEntry $sourceEntry, string $to) {
600
		$cache = $this->getCache();
601
602
		$sourceUrn = $this->getURN($sourceEntry->getId());
603
604
		if (!$cache instanceof Cache) {
0 ignored issues
show
introduced by
$cache is always a sub-type of OC\Files\Cache\Cache.
Loading history...
605
			throw new \Exception("Invalid source cache for object store copy");
606
		}
607
608
		$targetId = $cache->copyFromCache($cache, $sourceEntry, $to);
609
610
		$targetUrn = $this->getURN($targetId);
611
612
		try {
613
			$this->objectStore->copyObject($sourceUrn, $targetUrn);
614
		} catch (\Exception $e) {
615
			$cache->remove($to);
616
617
			throw $e;
618
		}
619
	}
620
}
621