Completed
Pull Request — master (#32044)
by Thomas
19:39
created

ObjectStoreStorage::unlink()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 5
nop 1
dl 0
loc 23
rs 8.9297
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Joas Schilling <[email protected]>
4
 * @author Jörn Friedrich Dreyer <[email protected]>
5
 * @author Morris Jobke <[email protected]>
6
 * @author Robin Appelman <[email protected]>
7
 * @author Thomas Müller <[email protected]>
8
 *
9
 * @copyright Copyright (c) 2018, ownCloud GmbH
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 function GuzzleHttp\Psr7\stream_for;
29
use Icewind\Streams\IteratorDirectory;
30
use OC\Files\Cache\CacheEntry;
31
use OC\Files\Storage\Common;
32
use OCP\Files\NotFoundException;
33
use OCP\Files\ObjectStore\IObjectStore;
34
use OCP\Files\ObjectStore\IVersionedObjectStorage;
35
use OCP\Files\Storage\IStorage;
36
use Psr\Http\Message\StreamInterface;
37
38
class ObjectStoreStorage extends Common {
39
40
	/**
41
	 * @var array
42
	 */
43
	private static $tmpFiles = [];
44
	/**
45
	 * @var \OCP\Files\ObjectStore\IObjectStore $objectStore
46
	 */
47
	protected $objectStore;
48
	/**
49
	 * @var string $id
50
	 */
51
	protected $id;
52
	/**
53
	 * @var \OC\User\User $user
54
	 */
55
	protected $user;
56
57
	private $objectPrefix = 'urn:oid:';
58
59
	public function __construct($params) {
60
		if (isset($params['objectstore']) && $params['objectstore'] instanceof IObjectStore) {
61
			$this->objectStore = $params['objectstore'];
62
		} else {
63
			throw new \Exception('missing IObjectStore instance');
64
		}
65
		if (isset($params['storageid'])) {
66
			$this->id = 'object::store:' . $params['storageid'];
67
		} else {
68
			$this->id = 'object::store:' . $this->objectStore->getStorageId();
69
		}
70
		if (isset($params['objectPrefix'])) {
71
			$this->objectPrefix = $params['objectPrefix'];
0 ignored issues
show
Documentation Bug introduced by
It seems like $params['objectPrefix'] of type object<OCP\Files\ObjectStore\IObjectStore> is incompatible with the declared type string of property $objectPrefix.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
72
		}
73
		//initialize cache with root directory in cache
74
		if (!$this->is_dir('/')) {
75
			$this->mkdir('/');
76
		}
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 View Code Duplication
	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
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
		$this->rmObjects($path);
169
170
		$this->getCache()->remove($path);
171
172
		return true;
173
	}
174
175
	private function rmObjects($path) {
176
		$children = $this->getCache()->getFolderContents($path);
177
		foreach ($children as $child) {
178
			if ($child['mimetype'] === 'httpd/unix-directory') {
179
				$this->rmObjects($child['path']);
180
			} else {
181
				$this->unlink($child['path']);
182
			}
183
		}
184
	}
185
186
	public function unlink($path) {
187
		$path = $this->normalizePath($path);
188
		$stat = $this->stat($path);
189
190
		if ($stat && isset($stat['fileid'])) {
191
			if ($stat['mimetype'] === 'httpd/unix-directory') {
192
				return $this->rmdir($path);
193
			}
194
			try {
195
				$this->objectStore->deleteObject($this->getURN($stat['fileid']));
196
			} catch (\Exception $ex) {
197
				if ($ex->getCode() !== 404) {
198
					\OCP\Util::writeLog('objectstore', 'Could not delete object: ' . $ex->getMessage(), \OCP\Util::ERROR);
199
					return false;
200
				} else {
201
					//removing from cache is ok as it does not exist in the objectstore anyway
202
				}
203
			}
204
			$this->getCache()->remove($path);
205
			return true;
206
		}
207
		return false;
208
	}
209
210
	public function stat($path) {
211
		$path = $this->normalizePath($path);
212
		$cacheEntry = $this->getCache()->get($path);
213
		if ($cacheEntry instanceof CacheEntry) {
214
			return $cacheEntry->getData();
215
		} else {
216
			return false;
217
		}
218
	}
219
220
	/**
221
	 * Override this method if you need a different unique resource identifier for your object storage implementation.
222
	 * The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users.
223
	 * You may need a mapping table to store your URN if it cannot be generated from the fileid.
224
	 *
225
	 * @param int $fileId the fileid
226
	 * @return null|string the unified resource name used to identify the object
227
	 */
228
	protected function getURN($fileId) {
229
		if (\is_numeric($fileId)) {
230
			return $this->objectPrefix . $fileId;
231
		}
232
		return null;
233
	}
234
235
	public function opendir($path) {
236
		$path = $this->normalizePath($path);
237
238
		try {
239
			$files = [];
240
			$folderContents = $this->getCache()->getFolderContents($path);
241
			foreach ($folderContents as $file) {
242
				$files[] = $file['name'];
243
			}
244
245
			return IteratorDirectory::wrap($files);
246
		} catch (\Exception $e) {
247
			\OCP\Util::writeLog('objectstore', $e->getMessage(), \OCP\Util::ERROR);
248
			return false;
249
		}
250
	}
251
252
	public function filetype($path) {
253
		$path = $this->normalizePath($path);
254
		$stat = $this->stat($path);
255
		if ($stat) {
256
			if ($stat['mimetype'] === 'httpd/unix-directory') {
257
				return 'dir';
258
			}
259
			return 'file';
260
		} else {
261
			return false;
262
		}
263
	}
264
265
	public function fopen($path, $mode) {
266
		throw new \BadMethodCallException('fopen is no longer allowed to be called');
267
	}
268
269
	public function file_exists($path) {
270
		$path = $this->normalizePath($path);
271
		return (bool)$this->stat($path);
272
	}
273
274
	public function rename($source, $target) {
275
		$source = $this->normalizePath($source);
276
		$target = $this->normalizePath($target);
277
		$this->remove($target);
278
		$this->getCache()->move($source, $target);
279
		$this->touch(\dirname($target));
280
		return true;
281
	}
282
283
	public function moveFromStorage(\OCP\Files\Storage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
284
		if ($sourceStorage === $this) {
285
			return $this->copy($sourceInternalPath, $targetInternalPath);
286
		}
287
		// cross storage moves need to perform a move operation
288
		// TODO: there is some cache updating missing which requires bigger changes and is
289
		//       subject to followup PRs
290
		if (!$sourceStorage->instanceOfStorage(self::class)) {
291
			return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
292
		}
293
294
		// source and target live on the same object store and we can simply rename
295
		// which updates the cache properly
296
		$this->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
297
		return true;
298
	}
299
300
	public function getMimeType($path) {
301
		$path = $this->normalizePath($path);
302
		$stat = $this->stat($path);
303
		if (\is_array($stat)) {
304
			return $stat['mimetype'];
305
		} else {
306
			return false;
307
		}
308
	}
309
310
	public function touch($path, $mtime = null) {
311
		if ($mtime === null) {
312
			$mtime = \time();
313
		}
314
315
		$path = $this->normalizePath($path);
316
		$dirName = \dirname($path);
317
		$parentExists = $this->is_dir($dirName);
318
		if (!$parentExists) {
319
			return false;
320
		}
321
322
		$stat = $this->stat($path);
323
		if (\is_array($stat)) {
324
			// update existing mtime in db
325
			$stat['mtime'] = $mtime;
326
			$this->getCache()->update($stat['fileid'], $stat);
327
		} else {
328
			$mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
329
			// create new file
330
			$stat = [
331
				'etag' => $this->getETag($path),
332
				'mimetype' => $mimeType,
333
				'size' => 0,
334
				'mtime' => $mtime,
335
				'storage_mtime' => $mtime,
336
				'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
337
			];
338
			$fileId = $this->getCache()->put($path, $stat);
339
			try {
340
				//read an empty file from memory
341
				$this->objectStore->writeObject($this->getURN($fileId), \fopen('php://memory', 'r'));
342
			} catch (\Exception $ex) {
343
				$this->getCache()->remove($path);
344
				\OCP\Util::writeLog('objectstore', 'Could not create object: ' . $ex->getMessage(), \OCP\Util::ERROR);
345
				return false;
346
			}
347
		}
348
		return true;
349
	}
350
351
	/**
352
	 * external changes are not supported, exclusive access to the object storage is assumed
353
	 *
354
	 * @param string $path
355
	 * @param int $time
356
	 * @return false
357
	 */
358
	public function hasUpdated($path, $time) {
359
		return false;
360
	}
361
362 View Code Duplication
	public function saveVersion($internalPath) {
363
		if ($this->objectStore instanceof IVersionedObjectStorage) {
364
			$stat = $this->stat($internalPath);
365
			// There are cases in the current implementation where saveVersion
366
			// is called before the file was even written.
367
			// There is nothing to be done in this case.
368
			// We return true to not trigger the fallback implementation
369
			if ($stat === false) {
370
				return true;
371
			}
372
			return $this->objectStore->saveVersion($this->getURN($stat['fileid']));
373
		}
374
		return parent::saveVersion($internalPath);
375
	}
376
377
	public function getVersions($internalPath) {
378
		if ($this->objectStore instanceof IVersionedObjectStorage) {
379
			$stat = $this->stat($internalPath);
380
			if ($stat === false) {
381
				throw new NotFoundException();
382
			}
383
			$versions = $this->objectStore->getVersions($this->getURN($stat['fileid']));
384
			list($uid, $path) = $this->convertInternalPathToGlobalPath($internalPath);
385
			return \array_map(function (array $version) use ($uid, $path) {
386
				$version['path'] = $path;
387
				$version['owner'] = $uid;
388
				return $version;
389
			}, $versions);
390
		}
391
		return parent::getVersions($internalPath);
392
	}
393
394
	public function getVersion($internalPath, $versionId) {
395
		if ($this->objectStore instanceof IVersionedObjectStorage) {
396
			$stat = $this->stat($internalPath);
397
			if ($stat === false) {
398
				throw new NotFoundException();
399
			}
400
			$version = $this->objectStore->getVersion($this->getURN($stat['fileid']), $versionId);
401
			list($uid, $path) = $this->convertInternalPathToGlobalPath($internalPath);
402
			if (!empty($version)) {
403
				$version['path'] = $path;
404
				$version['owner'] = $uid;
405
			}
406
			return $version;
407
		}
408
		return parent::getVersion($internalPath, $versionId);
409
	}
410
411 View Code Duplication
	public function getContentOfVersion($internalPath, $versionId) {
412
		if ($this->objectStore instanceof IVersionedObjectStorage) {
413
			$stat = $this->stat($internalPath);
414
			if ($stat === false) {
415
				throw new NotFoundException();
416
			}
417
			return $this->objectStore->getContentOfVersion($this->getURN($stat['fileid']), $versionId);
418
		}
419
		return parent::getContentOfVersion($internalPath, $versionId);
420
	}
421
422 View Code Duplication
	public function restoreVersion($internalPath, $versionId) {
423
		if ($this->objectStore instanceof IVersionedObjectStorage) {
424
			$stat = $this->stat($internalPath);
425
			if ($stat === false) {
426
				throw new NotFoundException();
427
			}
428
			return $this->objectStore->restoreVersion($this->getURN($stat['fileid']), $versionId);
429
		}
430
		return parent::restoreVersion($internalPath, $versionId);
431
	}
432
433
	public function readFile(string $path, array $options = []): StreamInterface {
434
		$stat = $this->stat($path);
435
		if ($stat === false) {
436
			throw new NotFoundException();
437
		}
438
		return stream_for($this->objectStore->readObject($this->getURN($stat['fileid'])));
439
	}
440
441
	public function writeFile(string $path, StreamInterface $stream): int {
442
		$stat = $this->stat($path);
443
		if (empty($stat)) {
444
			// create new file
445
			$stat = [
446
				'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
447
			];
448
		}
449
		// update stat with new data
450
		$mTime = \time();
451
		$stat['size'] = $stream->getSize();
452
		$stat['mtime'] = $mTime;
453
		$stat['storage_mtime'] = $mTime;
454
		$stat['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($path);
455
		$stat['etag'] = $this->getETag($path);
456
457
		$fileId = $this->getCache()->put($path, $stat);
458
		try {
459
			//upload to object storage
460
			$this->objectStore->writeObject($this->getURN($fileId), $stream->detach());
0 ignored issues
show
Bug introduced by
It seems like $stream->detach() targeting Psr\Http\Message\StreamInterface::detach() can also be of type null; however, OCP\Files\ObjectStore\IObjectStore::writeObject() does only seem to accept resource, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
461
		} catch (\Exception $ex) {
462
			$this->getCache()->remove($path);
463
			\OCP\Util::writeLog('objectstore', 'Could not create object: ' . $ex->getMessage(), \OCP\Util::ERROR);
464
			throw $ex; // make this bubble up
465
		}
466
		return $stat['size'];
467
	}
468
}
469