Passed
Push — master ( 8113df...5a27e5 )
by Morris
27:51 queued 12:24
created

ObjectStoreStorage::stat()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Joas Schilling <[email protected]>
6
 * @author Jörn Friedrich Dreyer <[email protected]>
7
 * @author Morris Jobke <[email protected]>
8
 * @author Robin Appelman <[email protected]>
9
 *
10
 * @license AGPL-3.0
11
 *
12
 * This code is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License, version 3,
14
 * as published by the Free Software Foundation.
15
 *
16
 * This program is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
 * GNU Affero General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU Affero General Public License, version 3,
22
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
23
 *
24
 */
25
26
namespace OC\Files\ObjectStore;
27
28
use Icewind\Streams\CallbackWrapper;
29
use Icewind\Streams\IteratorDirectory;
30
use OC\Files\Cache\CacheEntry;
31
use OC\Files\Stream\CountReadStream;
32
use OCP\Files\NotFoundException;
33
use OCP\Files\ObjectStore\IObjectStore;
34
35
class ObjectStoreStorage extends \OC\Files\Storage\Common {
36
	/**
37
	 * @var \OCP\Files\ObjectStore\IObjectStore $objectStore
38
	 */
39
	protected $objectStore;
40
	/**
41
	 * @var string $id
42
	 */
43
	protected $id;
44
	/**
45
	 * @var \OC\User\User $user
46
	 */
47
	protected $user;
48
49
	private $objectPrefix = 'urn:oid:';
50
51
	private $logger;
52
53
	public function __construct($params) {
54
		if (isset($params['objectstore']) && $params['objectstore'] instanceof IObjectStore) {
55
			$this->objectStore = $params['objectstore'];
56
		} else {
57
			throw new \Exception('missing IObjectStore instance');
58
		}
59
		if (isset($params['storageid'])) {
60
			$this->id = 'object::store:' . $params['storageid'];
61
		} else {
62
			$this->id = 'object::store:' . $this->objectStore->getStorageId();
63
		}
64
		if (isset($params['objectPrefix'])) {
65
			$this->objectPrefix = $params['objectPrefix'];
66
		}
67
		//initialize cache with root directory in cache
68
		if (!$this->is_dir('/')) {
69
			$this->mkdir('/');
70
		}
71
72
		$this->logger = \OC::$server->getLogger();
73
	}
74
75
	public function mkdir($path) {
76
		$path = $this->normalizePath($path);
77
78
		if ($this->file_exists($path)) {
79
			return false;
80
		}
81
82
		$mTime = time();
83
		$data = [
84
			'mimetype' => 'httpd/unix-directory',
85
			'size' => 0,
86
			'mtime' => $mTime,
87
			'storage_mtime' => $mTime,
88
			'permissions' => \OCP\Constants::PERMISSION_ALL,
89
		];
90
		if ($path === '') {
91
			//create root on the fly
92
			$data['etag'] = $this->getETag('');
93
			$this->getCache()->put('', $data);
94
			return true;
95
		} else {
96
			// if parent does not exist, create it
97
			$parent = $this->normalizePath(dirname($path));
98
			$parentType = $this->filetype($parent);
99
			if ($parentType === false) {
100
				if (!$this->mkdir($parent)) {
101
					// something went wrong
102
					return false;
103
				}
104
			} else if ($parentType === 'file') {
105
				// parent is a file
106
				return false;
107
			}
108
			// finally create the new dir
109
			$mTime = time(); // update mtime
110
			$data['mtime'] = $mTime;
111
			$data['storage_mtime'] = $mTime;
112
			$data['etag'] = $this->getETag($path);
113
			$this->getCache()->put($path, $data);
114
			return true;
115
		}
116
	}
117
118
	/**
119
	 * @param string $path
120
	 * @return string
121
	 */
122
	private function normalizePath($path) {
123
		$path = trim($path, '/');
124
		//FIXME why do we sometimes get a path like 'files//username'?
125
		$path = str_replace('//', '/', $path);
126
127
		// dirname('/folder') returns '.' but internally (in the cache) we store the root as ''
128
		if (!$path || $path === '.') {
129
			$path = '';
130
		}
131
132
		return $path;
133
	}
134
135
	/**
136
	 * Object Stores use a NoopScanner because metadata is directly stored in
137
	 * the file cache and cannot really scan the filesystem. The storage passed in is not used anywhere.
138
	 *
139
	 * @param string $path
140
	 * @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...
141
	 * @return \OC\Files\ObjectStore\NoopScanner
142
	 */
143
	public function getScanner($path = '', $storage = null) {
144
		if (!$storage) {
145
			$storage = $this;
146
		}
147
		if (!isset($this->scanner)) {
148
			$this->scanner = new NoopScanner($storage);
149
		}
150
		return $this->scanner;
151
	}
152
153
	public function getId() {
154
		return $this->id;
155
	}
156
157
	public function rmdir($path) {
158
		$path = $this->normalizePath($path);
159
160
		if (!$this->is_dir($path)) {
161
			return false;
162
		}
163
164
		$this->rmObjects($path);
165
166
		$this->getCache()->remove($path);
167
168
		return true;
169
	}
170
171
	private function rmObjects($path) {
172
		$children = $this->getCache()->getFolderContents($path);
173
		foreach ($children as $child) {
174
			if ($child['mimetype'] === 'httpd/unix-directory') {
175
				$this->rmObjects($child['path']);
176
			} else {
177
				$this->unlink($child['path']);
178
			}
179
		}
180
	}
181
182
	public function unlink($path) {
183
		$path = $this->normalizePath($path);
184
		$stat = $this->stat($path);
185
186
		if ($stat && isset($stat['fileid'])) {
187
			if ($stat['mimetype'] === 'httpd/unix-directory') {
188
				return $this->rmdir($path);
189
			}
190
			try {
191
				$this->objectStore->deleteObject($this->getURN($stat['fileid']));
192
			} catch (\Exception $ex) {
193
				if ($ex->getCode() !== 404) {
194
					$this->logger->logException($ex, [
195
						'app' => 'objectstore',
196
						'message' => 'Could not delete object ' . $this->getURN($stat['fileid']) . ' for ' . $path,
197
					]);
198
					return false;
199
				}
200
				//removing from cache is ok as it does not exist in the objectstore anyway
201
			}
202
			$this->getCache()->remove($path);
203
			return true;
204
		}
205
		return false;
206
	}
207
208
	public function stat($path) {
209
		$path = $this->normalizePath($path);
210
		$cacheEntry = $this->getCache()->get($path);
211
		if ($cacheEntry instanceof CacheEntry) {
212
			return $cacheEntry->getData();
213
		} else {
214
			return false;
215
		}
216
	}
217
218
	/**
219
	 * Override this method if you need a different unique resource identifier for your object storage implementation.
220
	 * The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users.
221
	 * You may need a mapping table to store your URN if it cannot be generated from the fileid.
222
	 *
223
	 * @param int $fileId the fileid
224
	 * @return null|string the unified resource name used to identify the object
225
	 */
226
	protected function getURN($fileId) {
227
		if (is_numeric($fileId)) {
0 ignored issues
show
introduced by
The condition is_numeric($fileId) is always true.
Loading history...
228
			return $this->objectPrefix . $fileId;
229
		}
230
		return null;
231
	}
232
233
	public function opendir($path) {
234
		$path = $this->normalizePath($path);
235
236
		try {
237
			$files = array();
238
			$folderContents = $this->getCache()->getFolderContents($path);
239
			foreach ($folderContents as $file) {
240
				$files[] = $file['name'];
241
			}
242
243
			return IteratorDirectory::wrap($files);
244
		} catch (\Exception $e) {
245
			$this->logger->logException($e);
246
			return false;
247
		}
248
	}
249
250
	public function filetype($path) {
251
		$path = $this->normalizePath($path);
252
		$stat = $this->stat($path);
253
		if ($stat) {
254
			if ($stat['mimetype'] === 'httpd/unix-directory') {
255
				return 'dir';
256
			}
257
			return 'file';
258
		} else {
259
			return false;
260
		}
261
	}
262
263
	public function fopen($path, $mode) {
264
		$path = $this->normalizePath($path);
265
266
		if (strrpos($path, '.') !== false) {
267
			$ext = substr($path, strrpos($path, '.'));
268
		} else {
269
			$ext = '';
270
		}
271
272
		switch ($mode) {
273
			case 'r':
274
			case 'rb':
275
				$stat = $this->stat($path);
276
				if (is_array($stat)) {
277
					try {
278
						return $this->objectStore->readObject($this->getURN($stat['fileid']));
279
					} catch (NotFoundException $e) {
280
						$this->logger->logException($e, [
281
							'app' => 'objectstore',
282
							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
283
						]);
284
						throw $e;
285
					} catch (\Exception $ex) {
286
						$this->logger->logException($ex, [
287
							'app' => 'objectstore',
288
							'message' => 'Could not get object ' . $this->getURN($stat['fileid']) . ' for file ' . $path,
289
						]);
290
						return false;
291
					}
292
				} else {
293
					return false;
294
				}
295
			case 'w':
296
			case 'wb':
297
			case 'w+':
298
			case 'wb+':
299
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
300
				$handle = fopen($tmpFile, $mode);
301
				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
0 ignored issues
show
Bug introduced by
It seems like $handle can also be of type false; however, parameter $source of Icewind\Streams\CallbackWrapper::wrap() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

301
				return CallbackWrapper::wrap(/** @scrutinizer ignore-type */ $handle, null, null, function () use ($path, $tmpFile) {
Loading history...
302
					$this->writeBack($tmpFile, $path);
303
				});
304
			case 'a':
305
			case 'ab':
306
			case 'r+':
307
			case 'a+':
308
			case 'x':
309
			case 'x+':
310
			case 'c':
311
			case 'c+':
312
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
313
				if ($this->file_exists($path)) {
314
					$source = $this->fopen($path, 'r');
315
					file_put_contents($tmpFile, $source);
316
				}
317
				$handle = fopen($tmpFile, $mode);
318
				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
319
					$this->writeBack($tmpFile, $path);
320
				});
321
		}
322
		return false;
323
	}
324
325
	public function file_exists($path) {
326
		$path = $this->normalizePath($path);
327
		return (bool)$this->stat($path);
328
	}
329
330
	public function rename($source, $target) {
331
		$source = $this->normalizePath($source);
332
		$target = $this->normalizePath($target);
333
		$this->remove($target);
334
		$this->getCache()->move($source, $target);
335
		$this->touch(dirname($target));
336
		return true;
337
	}
338
339
	public function getMimeType($path) {
340
		$path = $this->normalizePath($path);
341
		$stat = $this->stat($path);
342
		if (is_array($stat)) {
343
			return $stat['mimetype'];
344
		} else {
345
			return false;
346
		}
347
	}
348
349
	public function touch($path, $mtime = null) {
350
		if (is_null($mtime)) {
351
			$mtime = time();
352
		}
353
354
		$path = $this->normalizePath($path);
355
		$dirName = dirname($path);
356
		$parentExists = $this->is_dir($dirName);
357
		if (!$parentExists) {
358
			return false;
359
		}
360
361
		$stat = $this->stat($path);
362
		if (is_array($stat)) {
363
			// update existing mtime in db
364
			$stat['mtime'] = $mtime;
365
			$this->getCache()->update($stat['fileid'], $stat);
366
		} else {
367
			try {
368
				//create a empty file, need to have at least on char to make it
369
				// work with all object storage implementations
370
				$this->file_put_contents($path, ' ');
371
				$mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
372
				$stat = array(
373
					'etag' => $this->getETag($path),
374
					'mimetype' => $mimeType,
375
					'size' => 0,
376
					'mtime' => $mtime,
377
					'storage_mtime' => $mtime,
378
					'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
379
				);
380
				$this->getCache()->put($path, $stat);
381
			} catch (\Exception $ex) {
382
				$this->logger->logException($ex, [
383
					'app' => 'objectstore',
384
					'message' => 'Could not create object for ' . $path,
385
				]);
386
				throw $ex;
387
			}
388
		}
389
		return true;
390
	}
391
392
	public function writeBack($tmpFile, $path) {
393
		$size = filesize($tmpFile);
394
		$this->writeStream($path, fopen($tmpFile, 'r'), $size);
395
	}
396
397
	/**
398
	 * external changes are not supported, exclusive access to the object storage is assumed
399
	 *
400
	 * @param string $path
401
	 * @param int $time
402
	 * @return false
403
	 */
404
	public function hasUpdated($path, $time) {
405
		return false;
406
	}
407
408
	public function needsPartFile() {
409
		return false;
410
	}
411
412
	public function file_put_contents($path, $data) {
413
		$stream = fopen('php://temp', 'r+');
414
		fwrite($stream, $data);
0 ignored issues
show
Bug introduced by
It seems like $stream can also be of type false; however, parameter $handle of fwrite() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

414
		fwrite(/** @scrutinizer ignore-type */ $stream, $data);
Loading history...
415
		rewind($stream);
0 ignored issues
show
Bug introduced by
It seems like $stream can also be of type false; however, parameter $handle of rewind() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

415
		rewind(/** @scrutinizer ignore-type */ $stream);
Loading history...
416
		return $this->writeStream($path, $stream, strlen($data)) > 0;
417
	}
418
419
	public function writeStream(string $path, $stream, int $size = null): int {
420
		$stat = $this->stat($path);
421
		if (empty($stat)) {
422
			// create new file
423
			$stat = [
424
				'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
425
			];
426
		}
427
		// update stat with new data
428
		$mTime = time();
429
		$stat['size'] = (int)$size;
430
		$stat['mtime'] = $mTime;
431
		$stat['storage_mtime'] = $mTime;
432
433
		$mimetypeDetector = \OC::$server->getMimeTypeDetector();
434
		$mimetype = $mimetypeDetector->detectPath($path);
435
436
		$stat['mimetype'] = $mimetype;
437
		$stat['etag'] = $this->getETag($path);
438
439
		$exists = $this->getCache()->inCache($path);
440
		$uploadPath = $exists ? $path : $path . '.part';
441
		$fileId = $this->getCache()->put($uploadPath, $stat);
442
		$urn = $this->getURN($fileId);
443
		try {
444
			//upload to object storage
445
			if ($size === null) {
446
				$countStream = CountReadStream::wrap($stream, function ($writtenSize) use ($fileId, &$size) {
447
					$this->getCache()->update($fileId, [
448
						'size' => $writtenSize
449
					]);
450
					$size = $writtenSize;
451
				});
452
				$this->objectStore->writeObject($urn, $countStream);
0 ignored issues
show
Bug introduced by
It seems like $countStream can also be of type false; however, parameter $stream of OCP\Files\ObjectStore\IObjectStore::writeObject() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

452
				$this->objectStore->writeObject($urn, /** @scrutinizer ignore-type */ $countStream);
Loading history...
453
				if (is_resource($countStream)) {
454
					fclose($countStream);
455
				}
456
			} else {
457
				$this->objectStore->writeObject($urn, $stream);
458
			}
459
		} catch (\Exception $ex) {
460
			$this->getCache()->remove($uploadPath);
461
			$this->logger->logException($ex, [
462
				'app' => 'objectstore',
463
				'message' => 'Could not create object ' . $urn . ' for ' . $path,
464
			]);
465
			throw $ex; // make this bubble up
466
		}
467
468
		if (!$exists) {
469
			if ($this->objectStore->objectExists($urn)) {
470
				$this->getCache()->move($uploadPath, $path);
471
			} else {
472
				$this->getCache()->remove($uploadPath);
473
				throw new \Exception("Object not found after writing (urn: $urn, path: $path)", 404);
474
			}
475
		}
476
477
		return $size;
478
	}
479
}
480