Passed
Push — master ( 6b835e...8dd249 )
by Roeland
14:09
created

ObjectStoreStorage   F

Complexity

Total Complexity 86

Size/Duplication

Total Lines 471
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 264
c 2
b 0
f 0
dl 0
loc 471
rs 2
wmc 86

23 Methods

Rating   Name   Duplication   Size   Complexity  
A touch() 0 41 5
A needsPartFile() 0 2 1
A file_put_contents() 0 5 1
A writeBack() 0 3 1
A hasUpdated() 0 2 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 unlink() 0 24 6
A normalizePath() 0 11 3
A getURN() 0 5 2
A rmObjects() 0 15 5
A filetype() 0 10 3
A opendir() 0 14 3
A rmdir() 0 14 3
A getMimeType() 0 3 1
D fopen() 0 61 20
A rename() 0 7 1
A file_exists() 0 3 1
A getObjectStore() 0 2 1
C writeStream() 0 79 10

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
 *
14
 * @license AGPL-3.0
15
 *
16
 * This code is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU Affero General Public License, version 3,
18
 * as published by the Free Software Foundation.
19
 *
20
 * This program is distributed in the hope that it will be useful,
21
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23
 * GNU Affero General Public License for more details.
24
 *
25
 * You should have received a copy of the GNU Affero General Public License, version 3,
26
 * along with this program. If not, see <http://www.gnu.org/licenses/>
27
 *
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\CacheEntry;
36
use OCP\Files\NotFoundException;
37
use OCP\Files\ObjectStore\IObjectStore;
38
39
class ObjectStoreStorage extends \OC\Files\Storage\Common {
40
	/**
41
	 * @var \OCP\Files\ObjectStore\IObjectStore $objectStore
42
	 */
43
	protected $objectStore;
44
	/**
45
	 * @var string $id
46
	 */
47
	protected $id;
48
	/**
49
	 * @var \OC\User\User $user
50
	 */
51
	protected $user;
52
53
	private $objectPrefix = 'urn:oid:';
54
55
	private $logger;
56
57
	public function __construct($params) {
58
		if (isset($params['objectstore']) && $params['objectstore'] instanceof IObjectStore) {
59
			$this->objectStore = $params['objectstore'];
60
		} else {
61
			throw new \Exception('missing IObjectStore instance');
62
		}
63
		if (isset($params['storageid'])) {
64
			$this->id = 'object::store:' . $params['storageid'];
65
		} else {
66
			$this->id = 'object::store:' . $this->objectStore->getStorageId();
67
		}
68
		if (isset($params['objectPrefix'])) {
69
			$this->objectPrefix = $params['objectPrefix'];
70
		}
71
		//initialize cache with root directory in cache
72
		if (!$this->is_dir('/')) {
73
			$this->mkdir('/');
74
		}
75
76
		$this->logger = \OC::$server->getLogger();
0 ignored issues
show
Deprecated Code introduced by
The function OC\Server::getLogger() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

76
		$this->logger = /** @scrutinizer ignore-deprecated */ \OC::$server->getLogger();
Loading history...
77
	}
78
79
	public function mkdir($path) {
80
		$path = $this->normalizePath($path);
81
82
		if ($this->file_exists($path)) {
83
			return false;
84
		}
85
86
		$mTime = time();
87
		$data = [
88
			'mimetype' => 'httpd/unix-directory',
89
			'size' => 0,
90
			'mtime' => $mTime,
91
			'storage_mtime' => $mTime,
92
			'permissions' => \OCP\Constants::PERMISSION_ALL,
93
		];
94
		if ($path === '') {
95
			//create root on the fly
96
			$data['etag'] = $this->getETag('');
97
			$this->getCache()->put('', $data);
98
			return true;
99
		} else {
100
			// if parent does not exist, create it
101
			$parent = $this->normalizePath(dirname($path));
102
			$parentType = $this->filetype($parent);
103
			if ($parentType === false) {
104
				if (!$this->mkdir($parent)) {
105
					// something went wrong
106
					return false;
107
				}
108
			} elseif ($parentType === 'file') {
109
				// parent is a file
110
				return false;
111
			}
112
			// finally create the new dir
113
			$mTime = time(); // update mtime
114
			$data['mtime'] = $mTime;
115
			$data['storage_mtime'] = $mTime;
116
			$data['etag'] = $this->getETag($path);
117
			$this->getCache()->put($path, $data);
118
			return true;
119
		}
120
	}
121
122
	/**
123
	 * @param string $path
124
	 * @return string
125
	 */
126
	private function normalizePath($path) {
127
		$path = trim($path, '/');
128
		//FIXME why do we sometimes get a path like 'files//username'?
129
		$path = str_replace('//', '/', $path);
130
131
		// dirname('/folder') returns '.' but internally (in the cache) we store the root as ''
132
		if (!$path || $path === '.') {
133
			$path = '';
134
		}
135
136
		return $path;
137
	}
138
139
	/**
140
	 * Object Stores use a NoopScanner because metadata is directly stored in
141
	 * the file cache and cannot really scan the filesystem. The storage passed in is not used anywhere.
142
	 *
143
	 * @param string $path
144
	 * @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...
145
	 * @return \OC\Files\ObjectStore\NoopScanner
146
	 */
147
	public function getScanner($path = '', $storage = null) {
148
		if (!$storage) {
149
			$storage = $this;
150
		}
151
		if (!isset($this->scanner)) {
152
			$this->scanner = new NoopScanner($storage);
153
		}
154
		return $this->scanner;
155
	}
156
157
	public function getId() {
158
		return $this->id;
159
	}
160
161
	public function rmdir($path) {
162
		$path = $this->normalizePath($path);
163
164
		if (!$this->is_dir($path)) {
165
			return false;
166
		}
167
168
		if (!$this->rmObjects($path)) {
169
			return false;
170
		}
171
172
		$this->getCache()->remove($path);
173
174
		return true;
175
	}
176
177
	private function rmObjects($path) {
178
		$children = $this->getCache()->getFolderContents($path);
179
		foreach ($children as $child) {
180
			if ($child['mimetype'] === 'httpd/unix-directory') {
181
				if (!$this->rmObjects($child['path'])) {
182
					return false;
183
				}
184
			} else {
185
				if (!$this->unlink($child['path'])) {
186
					return false;
187
				}
188
			}
189
		}
190
191
		return true;
192
	}
193
194
	public function unlink($path) {
195
		$path = $this->normalizePath($path);
196
		$stat = $this->stat($path);
197
198
		if ($stat && isset($stat['fileid'])) {
199
			if ($stat['mimetype'] === 'httpd/unix-directory') {
200
				return $this->rmdir($path);
201
			}
202
			try {
203
				$this->objectStore->deleteObject($this->getURN($stat['fileid']));
204
			} catch (\Exception $ex) {
205
				if ($ex->getCode() !== 404) {
206
					$this->logger->logException($ex, [
207
						'app' => 'objectstore',
208
						'message' => 'Could not delete object ' . $this->getURN($stat['fileid']) . ' for ' . $path,
209
					]);
210
					return false;
211
				}
212
				//removing from cache is ok as it does not exist in the objectstore anyway
213
			}
214
			$this->getCache()->remove($path);
215
			return true;
216
		}
217
		return false;
218
	}
219
220
	public function stat($path) {
221
		$path = $this->normalizePath($path);
222
		$cacheEntry = $this->getCache()->get($path);
223
		if ($cacheEntry instanceof CacheEntry) {
224
			return $cacheEntry->getData();
225
		} else {
226
			return false;
227
		}
228
	}
229
230
	/**
231
	 * Override this method if you need a different unique resource identifier for your object storage implementation.
232
	 * The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users.
233
	 * You may need a mapping table to store your URN if it cannot be generated from the fileid.
234
	 *
235
	 * @param int $fileId the fileid
236
	 * @return null|string the unified resource name used to identify the object
237
	 */
238
	public function getURN($fileId) {
239
		if (is_numeric($fileId)) {
0 ignored issues
show
introduced by
The condition is_numeric($fileId) is always true.
Loading history...
240
			return $this->objectPrefix . $fileId;
241
		}
242
		return null;
243
	}
244
245
	public function opendir($path) {
246
		$path = $this->normalizePath($path);
247
248
		try {
249
			$files = [];
250
			$folderContents = $this->getCache()->getFolderContents($path);
251
			foreach ($folderContents as $file) {
252
				$files[] = $file['name'];
253
			}
254
255
			return IteratorDirectory::wrap($files);
256
		} catch (\Exception $e) {
257
			$this->logger->logException($e);
258
			return false;
259
		}
260
	}
261
262
	public function filetype($path) {
263
		$path = $this->normalizePath($path);
264
		$stat = $this->stat($path);
265
		if ($stat) {
266
			if ($stat['mimetype'] === 'httpd/unix-directory') {
267
				return 'dir';
268
			}
269
			return 'file';
270
		} else {
271
			return false;
272
		}
273
	}
274
275
	public function fopen($path, $mode) {
276
		$path = $this->normalizePath($path);
277
278
		if (strrpos($path, '.') !== false) {
279
			$ext = substr($path, strrpos($path, '.'));
280
		} else {
281
			$ext = '';
282
		}
283
284
		switch ($mode) {
285
			case 'r':
286
			case 'rb':
287
				$stat = $this->stat($path);
288
				if (is_array($stat)) {
289
					try {
290
						return $this->objectStore->readObject($this->getURN($stat['fileid']));
291
					} catch (NotFoundException $e) {
292
						$this->logger->logException($e, [
293
							'app' => 'objectstore',
294
							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
295
						]);
296
						throw $e;
297
					} catch (\Exception $ex) {
298
						$this->logger->logException($ex, [
299
							'app' => 'objectstore',
300
							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
301
						]);
302
						return false;
303
					}
304
				} else {
305
					return false;
306
				}
307
				// no break
308
			case 'w':
309
			case 'wb':
310
			case 'w+':
311
			case 'wb+':
312
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
0 ignored issues
show
Deprecated Code introduced by
The function OC\Server::getTempManager() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

312
				$tmpFile = /** @scrutinizer ignore-deprecated */ \OC::$server->getTempManager()->getTemporaryFile($ext);
Loading history...
313
				$handle = fopen($tmpFile, $mode);
314
				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
315
					$this->writeBack($tmpFile, $path);
316
				});
317
			case 'a':
318
			case 'ab':
319
			case 'r+':
320
			case 'a+':
321
			case 'x':
322
			case 'x+':
323
			case 'c':
324
			case 'c+':
325
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
0 ignored issues
show
Deprecated Code introduced by
The function OC\Server::getTempManager() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

325
				$tmpFile = /** @scrutinizer ignore-deprecated */ \OC::$server->getTempManager()->getTemporaryFile($ext);
Loading history...
326
				if ($this->file_exists($path)) {
327
					$source = $this->fopen($path, 'r');
328
					file_put_contents($tmpFile, $source);
329
				}
330
				$handle = fopen($tmpFile, $mode);
331
				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
332
					$this->writeBack($tmpFile, $path);
333
				});
334
		}
335
		return false;
336
	}
337
338
	public function file_exists($path) {
339
		$path = $this->normalizePath($path);
340
		return (bool)$this->stat($path);
341
	}
342
343
	public function rename($source, $target) {
344
		$source = $this->normalizePath($source);
345
		$target = $this->normalizePath($target);
346
		$this->remove($target);
347
		$this->getCache()->move($source, $target);
348
		$this->touch(dirname($target));
349
		return true;
350
	}
351
352
	public function getMimeType($path) {
353
		$path = $this->normalizePath($path);
354
		return parent::getMimeType($path);
355
	}
356
357
	public function touch($path, $mtime = null) {
358
		if (is_null($mtime)) {
359
			$mtime = time();
360
		}
361
362
		$path = $this->normalizePath($path);
363
		$dirName = dirname($path);
364
		$parentExists = $this->is_dir($dirName);
365
		if (!$parentExists) {
366
			return false;
367
		}
368
369
		$stat = $this->stat($path);
370
		if (is_array($stat)) {
371
			// update existing mtime in db
372
			$stat['mtime'] = $mtime;
373
			$this->getCache()->update($stat['fileid'], $stat);
374
		} else {
375
			try {
376
				//create a empty file, need to have at least on char to make it
377
				// work with all object storage implementations
378
				$this->file_put_contents($path, ' ');
379
				$mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
0 ignored issues
show
Deprecated Code introduced by
The function OC\Server::getMimeTypeDetector() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

379
				$mimeType = /** @scrutinizer ignore-deprecated */ \OC::$server->getMimeTypeDetector()->detectPath($path);
Loading history...
380
				$stat = [
381
					'etag' => $this->getETag($path),
382
					'mimetype' => $mimeType,
383
					'size' => 0,
384
					'mtime' => $mtime,
385
					'storage_mtime' => $mtime,
386
					'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
387
				];
388
				$this->getCache()->put($path, $stat);
389
			} catch (\Exception $ex) {
390
				$this->logger->logException($ex, [
391
					'app' => 'objectstore',
392
					'message' => 'Could not create object for ' . $path,
393
				]);
394
				throw $ex;
395
			}
396
		}
397
		return true;
398
	}
399
400
	public function writeBack($tmpFile, $path) {
401
		$size = filesize($tmpFile);
402
		$this->writeStream($path, fopen($tmpFile, 'r'), $size);
403
	}
404
405
	/**
406
	 * external changes are not supported, exclusive access to the object storage is assumed
407
	 *
408
	 * @param string $path
409
	 * @param int $time
410
	 * @return false
411
	 */
412
	public function hasUpdated($path, $time) {
413
		return false;
414
	}
415
416
	public function needsPartFile() {
417
		return false;
418
	}
419
420
	public function file_put_contents($path, $data) {
421
		$handle = $this->fopen($path, 'w+');
422
		fwrite($handle, $data);
423
		fclose($handle);
424
		return true;
425
	}
426
427
	public function writeStream(string $path, $stream, int $size = null): int {
428
		$stat = $this->stat($path);
429
		if (empty($stat)) {
430
			// create new file
431
			$stat = [
432
				'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
433
			];
434
		}
435
		// update stat with new data
436
		$mTime = time();
437
		$stat['size'] = (int)$size;
438
		$stat['mtime'] = $mTime;
439
		$stat['storage_mtime'] = $mTime;
440
441
		$mimetypeDetector = \OC::$server->getMimeTypeDetector();
0 ignored issues
show
Deprecated Code introduced by
The function OC\Server::getMimeTypeDetector() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

441
		$mimetypeDetector = /** @scrutinizer ignore-deprecated */ \OC::$server->getMimeTypeDetector();
Loading history...
442
		$mimetype = $mimetypeDetector->detectPath($path);
443
444
		$stat['mimetype'] = $mimetype;
445
		$stat['etag'] = $this->getETag($path);
446
447
		$exists = $this->getCache()->inCache($path);
448
		$uploadPath = $exists ? $path : $path . '.part';
449
450
		if ($exists) {
451
			$fileId = $stat['fileid'];
452
		} else {
453
			$fileId = $this->getCache()->put($uploadPath, $stat);
454
		}
455
456
		$urn = $this->getURN($fileId);
457
		try {
458
			//upload to object storage
459
			if ($size === null) {
460
				$countStream = CountWrapper::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
461
					$this->getCache()->update($fileId, [
462
						'size' => $writtenSize
463
					]);
464
					$size = $writtenSize;
465
				});
466
				$this->objectStore->writeObject($urn, $countStream);
467
				if (is_resource($countStream)) {
468
					fclose($countStream);
469
				}
470
				$stat['size'] = $size;
471
			} else {
472
				$this->objectStore->writeObject($urn, $stream);
473
			}
474
		} catch (\Exception $ex) {
475
			if (!$exists) {
476
				/*
477
				 * Only remove the entry if we are dealing with a new file.
478
				 * Else people lose access to existing files
479
				 */
480
				$this->getCache()->remove($uploadPath);
481
				$this->logger->logException($ex, [
482
					'app' => 'objectstore',
483
					'message' => 'Could not create object ' . $urn . ' for ' . $path,
484
				]);
485
			} else {
486
				$this->logger->logException($ex, [
487
					'app' => 'objectstore',
488
					'message' => 'Could not update object ' . $urn . ' for ' . $path,
489
				]);
490
			}
491
			throw $ex; // make this bubble up
492
		}
493
494
		if ($exists) {
495
			$this->getCache()->update($fileId, $stat);
496
		} else {
497
			if ($this->objectStore->objectExists($urn)) {
498
				$this->getCache()->move($uploadPath, $path);
499
			} else {
500
				$this->getCache()->remove($uploadPath);
501
				throw new \Exception("Object not found after writing (urn: $urn, path: $path)", 404);
502
			}
503
		}
504
505
		return $size;
506
	}
507
508
	public function getObjectStore(): IObjectStore {
509
		return $this->objectStore;
510
	}
511
}
512