Passed
Push — master ( bf39ad...32551b )
by Robin
14:52 queued 12s
created

ObjectStoreStorage   F

Complexity

Total Complexity 103

Size/Duplication

Total Lines 555
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 306
dl 0
loc 555
rs 2
c 0
b 0
f 0
wmc 103

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
A __construct() 0 20 6
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 66 22
A rmObjects() 0 15 5
A getURN() 0 5 2
A copy() 0 13 2
A copyFile() 0 20 3
A filetype() 0 10 3
A needsPartFile() 0 2 1
A opendir() 0 14 3
A rename() 0 7 1
C writeStream() 0 79 10
A file_exists() 0 3 1
A copyFromStorage() 0 11 3
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
31
namespace OC\Files\ObjectStore;
32
33
use Icewind\Streams\CallbackWrapper;
34
use Icewind\Streams\CountWrapper;
35
use Icewind\Streams\IteratorDirectory;
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
	public function __construct($params) {
65
		if (isset($params['objectstore']) && $params['objectstore'] instanceof IObjectStore) {
66
			$this->objectStore = $params['objectstore'];
67
		} else {
68
			throw new \Exception('missing IObjectStore instance');
69
		}
70
		if (isset($params['storageid'])) {
71
			$this->id = 'object::store:' . $params['storageid'];
72
		} else {
73
			$this->id = 'object::store:' . $this->objectStore->getStorageId();
74
		}
75
		if (isset($params['objectPrefix'])) {
76
			$this->objectPrefix = $params['objectPrefix'];
77
		}
78
		//initialize cache with root directory in cache
79
		if (!$this->is_dir('/')) {
80
			$this->mkdir('/');
81
		}
82
83
		$this->logger = \OC::$server->getLogger();
84
	}
85
86
	public function mkdir($path) {
87
		$path = $this->normalizePath($path);
88
89
		if ($this->file_exists($path)) {
90
			return false;
91
		}
92
93
		$mTime = time();
94
		$data = [
95
			'mimetype' => 'httpd/unix-directory',
96
			'size' => 0,
97
			'mtime' => $mTime,
98
			'storage_mtime' => $mTime,
99
			'permissions' => \OCP\Constants::PERMISSION_ALL,
100
		];
101
		if ($path === '') {
102
			//create root on the fly
103
			$data['etag'] = $this->getETag('');
104
			$this->getCache()->put('', $data);
105
			return true;
106
		} else {
107
			// if parent does not exist, create it
108
			$parent = $this->normalizePath(dirname($path));
109
			$parentType = $this->filetype($parent);
110
			if ($parentType === false) {
111
				if (!$this->mkdir($parent)) {
112
					// something went wrong
113
					return false;
114
				}
115
			} elseif ($parentType === 'file') {
116
				// parent is a file
117
				return false;
118
			}
119
			// finally create the new dir
120
			$mTime = time(); // update mtime
121
			$data['mtime'] = $mTime;
122
			$data['storage_mtime'] = $mTime;
123
			$data['etag'] = $this->getETag($path);
124
			$this->getCache()->put($path, $data);
125
			return true;
126
		}
127
	}
128
129
	/**
130
	 * @param string $path
131
	 * @return string
132
	 */
133
	private function normalizePath($path) {
134
		$path = trim($path, '/');
135
		//FIXME why do we sometimes get a path like 'files//username'?
136
		$path = str_replace('//', '/', $path);
137
138
		// dirname('/folder') returns '.' but internally (in the cache) we store the root as ''
139
		if (!$path || $path === '.') {
140
			$path = '';
141
		}
142
143
		return $path;
144
	}
145
146
	/**
147
	 * Object Stores use a NoopScanner because metadata is directly stored in
148
	 * the file cache and cannot really scan the filesystem. The storage passed in is not used anywhere.
149
	 *
150
	 * @param string $path
151
	 * @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...
152
	 * @return \OC\Files\ObjectStore\NoopScanner
153
	 */
154
	public function getScanner($path = '', $storage = null) {
155
		if (!$storage) {
156
			$storage = $this;
157
		}
158
		if (!isset($this->scanner)) {
159
			$this->scanner = new NoopScanner($storage);
160
		}
161
		return $this->scanner;
162
	}
163
164
	public function getId() {
165
		return $this->id;
166
	}
167
168
	public function rmdir($path) {
169
		$path = $this->normalizePath($path);
170
171
		if (!$this->is_dir($path)) {
172
			return false;
173
		}
174
175
		if (!$this->rmObjects($path)) {
176
			return false;
177
		}
178
179
		$this->getCache()->remove($path);
180
181
		return true;
182
	}
183
184
	private function rmObjects($path) {
185
		$children = $this->getCache()->getFolderContents($path);
186
		foreach ($children as $child) {
187
			if ($child['mimetype'] === 'httpd/unix-directory') {
188
				if (!$this->rmObjects($child['path'])) {
189
					return false;
190
				}
191
			} else {
192
				if (!$this->unlink($child['path'])) {
193
					return false;
194
				}
195
			}
196
		}
197
198
		return true;
199
	}
200
201
	public function unlink($path) {
202
		$path = $this->normalizePath($path);
203
		$stat = $this->stat($path);
204
205
		if ($stat && isset($stat['fileid'])) {
206
			if ($stat['mimetype'] === 'httpd/unix-directory') {
207
				return $this->rmdir($path);
208
			}
209
			try {
210
				$this->objectStore->deleteObject($this->getURN($stat['fileid']));
211
			} catch (\Exception $ex) {
212
				if ($ex->getCode() !== 404) {
213
					$this->logger->logException($ex, [
214
						'app' => 'objectstore',
215
						'message' => 'Could not delete object ' . $this->getURN($stat['fileid']) . ' for ' . $path,
216
					]);
217
					return false;
218
				}
219
				//removing from cache is ok as it does not exist in the objectstore anyway
220
			}
221
			$this->getCache()->remove($path);
222
			return true;
223
		}
224
		return false;
225
	}
226
227
	public function stat($path) {
228
		$path = $this->normalizePath($path);
229
		$cacheEntry = $this->getCache()->get($path);
230
		if ($cacheEntry instanceof CacheEntry) {
231
			return $cacheEntry->getData();
232
		} else {
233
			return false;
234
		}
235
	}
236
237
	public function getPermissions($path) {
238
		$stat = $this->stat($path);
239
240
		if (is_array($stat) && isset($stat['permissions'])) {
241
			return $stat['permissions'];
242
		}
243
244
		return parent::getPermissions($path);
245
	}
246
247
	/**
248
	 * Override this method if you need a different unique resource identifier for your object storage implementation.
249
	 * The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users.
250
	 * You may need a mapping table to store your URN if it cannot be generated from the fileid.
251
	 *
252
	 * @param int $fileId the fileid
253
	 * @return null|string the unified resource name used to identify the object
254
	 */
255
	public function getURN($fileId) {
256
		if (is_numeric($fileId)) {
0 ignored issues
show
introduced by
The condition is_numeric($fileId) is always true.
Loading history...
257
			return $this->objectPrefix . $fileId;
258
		}
259
		return null;
260
	}
261
262
	public function opendir($path) {
263
		$path = $this->normalizePath($path);
264
265
		try {
266
			$files = [];
267
			$folderContents = $this->getCache()->getFolderContents($path);
268
			foreach ($folderContents as $file) {
269
				$files[] = $file['name'];
270
			}
271
272
			return IteratorDirectory::wrap($files);
273
		} catch (\Exception $e) {
274
			$this->logger->logException($e);
275
			return false;
276
		}
277
	}
278
279
	public function filetype($path) {
280
		$path = $this->normalizePath($path);
281
		$stat = $this->stat($path);
282
		if ($stat) {
283
			if ($stat['mimetype'] === 'httpd/unix-directory') {
284
				return 'dir';
285
			}
286
			return 'file';
287
		} else {
288
			return false;
289
		}
290
	}
291
292
	public function fopen($path, $mode) {
293
		$path = $this->normalizePath($path);
294
295
		if (strrpos($path, '.') !== false) {
296
			$ext = substr($path, strrpos($path, '.'));
297
		} else {
298
			$ext = '';
299
		}
300
301
		switch ($mode) {
302
			case 'r':
303
			case 'rb':
304
				$stat = $this->stat($path);
305
				if (is_array($stat)) {
306
					// Reading 0 sized files is a waste of time
307
					if (isset($stat['size']) && $stat['size'] === 0) {
308
						return fopen('php://memory', $mode);
309
					}
310
311
					try {
312
						return $this->objectStore->readObject($this->getURN($stat['fileid']));
313
					} catch (NotFoundException $e) {
314
						$this->logger->logException($e, [
315
							'app' => 'objectstore',
316
							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
317
						]);
318
						throw $e;
319
					} catch (\Exception $ex) {
320
						$this->logger->logException($ex, [
321
							'app' => 'objectstore',
322
							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
323
						]);
324
						return false;
325
					}
326
				} else {
327
					return false;
328
				}
329
			// no break
330
			case 'w':
331
			case 'wb':
332
			case 'w+':
333
			case 'wb+':
334
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
335
				$handle = fopen($tmpFile, $mode);
336
				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
337
					$this->writeBack($tmpFile, $path);
338
				});
339
			case 'a':
340
			case 'ab':
341
			case 'r+':
342
			case 'a+':
343
			case 'x':
344
			case 'x+':
345
			case 'c':
346
			case 'c+':
347
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
348
				if ($this->file_exists($path)) {
349
					$source = $this->fopen($path, 'r');
350
					file_put_contents($tmpFile, $source);
351
				}
352
				$handle = fopen($tmpFile, $mode);
353
				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
354
					$this->writeBack($tmpFile, $path);
355
				});
356
		}
357
		return false;
358
	}
359
360
	public function file_exists($path) {
361
		$path = $this->normalizePath($path);
362
		return (bool)$this->stat($path);
363
	}
364
365
	public function rename($source, $target) {
366
		$source = $this->normalizePath($source);
367
		$target = $this->normalizePath($target);
368
		$this->remove($target);
369
		$this->getCache()->move($source, $target);
370
		$this->touch(dirname($target));
371
		return true;
372
	}
373
374
	public function getMimeType($path) {
375
		$path = $this->normalizePath($path);
376
		return parent::getMimeType($path);
377
	}
378
379
	public function touch($path, $mtime = null) {
380
		if (is_null($mtime)) {
381
			$mtime = time();
382
		}
383
384
		$path = $this->normalizePath($path);
385
		$dirName = dirname($path);
386
		$parentExists = $this->is_dir($dirName);
387
		if (!$parentExists) {
388
			return false;
389
		}
390
391
		$stat = $this->stat($path);
392
		if (is_array($stat)) {
393
			// update existing mtime in db
394
			$stat['mtime'] = $mtime;
395
			$this->getCache()->update($stat['fileid'], $stat);
396
		} else {
397
			try {
398
				//create a empty file, need to have at least on char to make it
399
				// work with all object storage implementations
400
				$this->file_put_contents($path, ' ');
401
				$mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
402
				$stat = [
403
					'etag' => $this->getETag($path),
404
					'mimetype' => $mimeType,
405
					'size' => 0,
406
					'mtime' => $mtime,
407
					'storage_mtime' => $mtime,
408
					'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
409
				];
410
				$this->getCache()->put($path, $stat);
411
			} catch (\Exception $ex) {
412
				$this->logger->logException($ex, [
413
					'app' => 'objectstore',
414
					'message' => 'Could not create object for ' . $path,
415
				]);
416
				throw $ex;
417
			}
418
		}
419
		return true;
420
	}
421
422
	public function writeBack($tmpFile, $path) {
423
		$size = filesize($tmpFile);
424
		$this->writeStream($path, fopen($tmpFile, 'r'), $size);
425
	}
426
427
	/**
428
	 * external changes are not supported, exclusive access to the object storage is assumed
429
	 *
430
	 * @param string $path
431
	 * @param int $time
432
	 * @return false
433
	 */
434
	public function hasUpdated($path, $time) {
435
		return false;
436
	}
437
438
	public function needsPartFile() {
439
		return false;
440
	}
441
442
	public function file_put_contents($path, $data) {
443
		$handle = $this->fopen($path, 'w+');
444
		$result = fwrite($handle, $data);
445
		fclose($handle);
446
		return $result;
447
	}
448
449
	public function writeStream(string $path, $stream, int $size = null): int {
450
		$stat = $this->stat($path);
451
		if (empty($stat)) {
452
			// create new file
453
			$stat = [
454
				'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
455
			];
456
		}
457
		// update stat with new data
458
		$mTime = time();
459
		$stat['size'] = (int)$size;
460
		$stat['mtime'] = $mTime;
461
		$stat['storage_mtime'] = $mTime;
462
463
		$mimetypeDetector = \OC::$server->getMimeTypeDetector();
464
		$mimetype = $mimetypeDetector->detectPath($path);
465
466
		$stat['mimetype'] = $mimetype;
467
		$stat['etag'] = $this->getETag($path);
468
469
		$exists = $this->getCache()->inCache($path);
470
		$uploadPath = $exists ? $path : $path . '.part';
471
472
		if ($exists) {
473
			$fileId = $stat['fileid'];
474
		} else {
475
			$fileId = $this->getCache()->put($uploadPath, $stat);
476
		}
477
478
		$urn = $this->getURN($fileId);
479
		try {
480
			//upload to object storage
481
			if ($size === null) {
482
				$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
483
					$this->getCache()->update($fileId, [
484
						'size' => $writtenSize,
485
					]);
486
					$size = $writtenSize;
487
				});
488
				$this->objectStore->writeObject($urn, $countStream);
489
				if (is_resource($countStream)) {
490
					fclose($countStream);
491
				}
492
				$stat['size'] = $size;
493
			} else {
494
				$this->objectStore->writeObject($urn, $stream);
495
			}
496
		} catch (\Exception $ex) {
497
			if (!$exists) {
498
				/*
499
				 * Only remove the entry if we are dealing with a new file.
500
				 * Else people lose access to existing files
501
				 */
502
				$this->getCache()->remove($uploadPath);
503
				$this->logger->logException($ex, [
504
					'app' => 'objectstore',
505
					'message' => 'Could not create object ' . $urn . ' for ' . $path,
506
				]);
507
			} else {
508
				$this->logger->logException($ex, [
509
					'app' => 'objectstore',
510
					'message' => 'Could not update object ' . $urn . ' for ' . $path,
511
				]);
512
			}
513
			throw $ex; // make this bubble up
514
		}
515
516
		if ($exists) {
517
			$this->getCache()->update($fileId, $stat);
518
		} else {
519
			if ($this->objectStore->objectExists($urn)) {
520
				$this->getCache()->move($uploadPath, $path);
521
			} else {
522
				$this->getCache()->remove($uploadPath);
523
				throw new \Exception("Object not found after writing (urn: $urn, path: $path)", 404);
524
			}
525
		}
526
527
		return $size;
528
	}
529
530
	public function getObjectStore(): IObjectStore {
531
		return $this->objectStore;
532
	}
533
534
	public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
535
		if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) {
536
			/** @var ObjectStoreStorage $sourceStorage */
537
			if ($sourceStorage->getObjectStore()->getStorageId() === $this->getObjectStore()->getStorageId()) {
538
				$sourceEntry = $sourceStorage->getCache()->get($sourceInternalPath);
539
				$this->copyInner($sourceEntry, $targetInternalPath);
540
				return true;
541
			}
542
		}
543
544
		return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
545
	}
546
547
	public function copy($path1, $path2) {
548
		$path1 = $this->normalizePath($path1);
549
		$path2 = $this->normalizePath($path2);
550
551
		$cache = $this->getCache();
552
		$sourceEntry = $cache->get($path1);
553
		if (!$sourceEntry) {
554
			throw new NotFoundException('Source object not found');
555
		}
556
557
		$this->copyInner($sourceEntry, $path2);
558
559
		return true;
560
	}
561
562
	private function copyInner(ICacheEntry $sourceEntry, string $to) {
563
		$cache = $this->getCache();
564
565
		if ($sourceEntry->getMimeType() === FileInfo::MIMETYPE_FOLDER) {
566
			if ($cache->inCache($to)) {
567
				$cache->remove($to);
568
			}
569
			$this->mkdir($to);
570
571
			foreach ($cache->getFolderContentsById($sourceEntry->getId()) as $child) {
572
				$this->copyInner($child, $to . '/' . $child->getName());
573
			}
574
		} else {
575
			$this->copyFile($sourceEntry, $to);
576
		}
577
	}
578
579
	private function copyFile(ICacheEntry $sourceEntry, string $to) {
580
		$cache = $this->getCache();
581
582
		$sourceUrn = $this->getURN($sourceEntry->getId());
583
584
		$cache->copyFromCache($cache, $sourceEntry, $to);
585
		$targetEntry = $cache->get($to);
586
587
		if (!$targetEntry) {
588
			throw new \Exception('Target not in cache after copy');
589
		}
590
591
		$targetUrn = $this->getURN($targetEntry->getId());
592
593
		try {
594
			$this->objectStore->copyObject($sourceUrn, $targetUrn);
595
		} catch (\Exception $e) {
596
			$cache->remove($to);
597
598
			throw $e;
599
		}
600
	}
601
}
602