Completed
Pull Request — master (#31958)
by Piotr
13:50
created

ObjectStoreStorage::copy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 10
rs 9.9332
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 Icewind\Streams\IteratorDirectory;
29
use OC\Files\Cache\CacheEntry;
30
use OC\Memcache\ArrayCache;
31
use OCP\Files\NotFoundException;
32
use OCP\Files\ObjectStore\IObjectStore;
33
use OCP\Files\ObjectStore\IVersionedObjectStorage;
34
35
class ObjectStoreStorage extends \OC\Files\Storage\Common {
36
37
	/**
38
	 * @var ArrayCache
39
	 */
40
	private $objectStatCache;
41
42
	/**
43
	 * @var array
44
	 */
45
	private static $tmpFiles = [];
46
	/**
47
	 * @var \OCP\Files\ObjectStore\IObjectStore $objectStore
48
	 */
49
	protected $objectStore;
50
	/**
51
	 * @var string $id
52
	 */
53
	protected $id;
54
	/**
55
	 * @var \OC\User\User $user
56
	 */
57
	protected $user;
58
59
	private $objectPrefix = 'urn:oid:';
60
61
	public function __construct($params) {
62
		$this->objectStatCache = new ArrayCache();
63
		if (isset($params['objectstore']) && $params['objectstore'] instanceof IObjectStore) {
64
			$this->objectStore = $params['objectstore'];
65
		} else {
66
			throw new \Exception('missing IObjectStore instance');
67
		}
68
		if (isset($params['storageid'])) {
69
			$this->id = 'object::store:' . $params['storageid'];
70
		} else {
71
			$this->id = 'object::store:' . $this->objectStore->getStorageId();
72
		}
73
		if (isset($params['objectPrefix'])) {
74
			$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...
75
		}
76
		//initialize cache with root directory in cache
77
		if (!$this->is_dir('/')) {
78
			$this->mkdir('/');
79
		}
80
	}
81
82
	/** {@inheritdoc} */
83
	public function mkdir($path) {
84
		$path = $this->normalizePath($path);
85
		$this->clearPathStat($path);
86
87
		if ($this->file_exists($path)) {
88
			return false;
89
		}
90
91
		$mTime = \time();
92
		$data = [
93
			'mimetype' => 'httpd/unix-directory',
94
			'size' => 0,
95
			'mtime' => $mTime,
96
			'storage_mtime' => $mTime,
97
			'permissions' => \OCP\Constants::PERMISSION_ALL,
98
		];
99
		if ($path === '') {
100
			//create root on the fly
101
			$data['etag'] = $this->getETag('');
102
			$this->getCache()->put('', $data);
103
			return true;
104
		} else {
105
			// if parent does not exist, create it
106
			$parent = $this->normalizePath(\dirname($path));
107
			$parentType = $this->filetype($parent);
108
			if ($parentType === false) {
109
				if (!$this->mkdir($parent)) {
110
					// something went wrong
111
					return false;
112
				}
113
			} elseif ($parentType === 'file') {
114
				// parent is a file
115
				return false;
116
			}
117
			// finally create the new dir
118
			$mTime = \time(); // update mtime
119
			$data['mtime'] = $mTime;
120
			$data['storage_mtime'] = $mTime;
121
			$data['etag'] = $this->getETag($path);
122
			$this->getCache()->put($path, $data);
123
			return true;
124
		}
125
	}
126
127
	/** {@inheritdoc} */
128
	public function getId() {
129
		return $this->id;
130
	}
131
132
	/** {@inheritdoc} */
133
	public function rmdir($path) {
134
		$path = $this->normalizePath($path);
135
136
		if (!$this->is_dir($path)) {
137
			return false;
138
		}
139
140
		// Clear stat path of all the children objects
141
		$this->clearPathStat($path);
142
143
		$this->rmObjects($path);
144
		$this->getCache()->remove($path);
145
146
		return true;
147
	}
148
149
	/** {@inheritdoc} */
150
	public function unlink($path) {
151
		$path = $this->normalizePath($path);
152
		$stat = $this->stat($path);
153
154
		if ($stat && isset($stat['fileid'])) {
155
			if ($stat['mimetype'] === 'httpd/unix-directory') {
156
				return $this->rmdir($path);
157
			}
158
159
			$this->removeObjectStat($path);
160
			try {
161
				$this->objectStore->deleteObject($this->getURN($stat['fileid']));
162
			} catch (\Exception $ex) {
163
				if ($ex->getCode() !== 404) {
164
					\OCP\Util::writeLog('objectstore', 'Could not delete object: ' . $ex->getMessage(), \OCP\Util::ERROR);
165
					return false;
166
				} else {
167
					//removing from cache is ok as it does not exist in the objectstore anyway
168
				}
169
			}
170
			$this->getCache()->remove($path);
171
			return true;
172
		}
173
		return false;
174
	}
175
176
	/** {@inheritdoc} */
177
	public function stat($path) {
178
		$path = $this->normalizePath($path);
179
		return $this->getPathStat($path);
180
	}
181
182
	/** {@inheritdoc} */
183
	public function opendir($path) {
184
		$path = $this->normalizePath($path);
185
186
		// We cannot use stat cache, so clear before returning folder contents
187
		$this->clearPathStat($path);
188
189
		try {
190
			$files = [];
191
			$folderContents = $this->getCache()->getFolderContents($path);
192
			foreach ($folderContents as $file) {
193
				$files[] = $file['name'];
194
			}
195
196
			return IteratorDirectory::wrap($files);
197
		} catch (\Exception $e) {
198
			\OCP\Util::writeLog('objectstore', $e->getMessage(), \OCP\Util::ERROR);
199
			return false;
200
		}
201
	}
202
203
	/** {@inheritdoc} */
204
	public function filetype($path) {
205
		$path = $this->normalizePath($path);
206
		$stat = $this->stat($path);
207
		if ($stat) {
208
			if ($stat['mimetype'] === 'httpd/unix-directory') {
209
				return 'dir';
210
			}
211
			return 'file';
212
		} else {
213
			return false;
214
		}
215
	}
216
217
	/** {@inheritdoc} */
218
	public function fopen($path, $mode) {
219
		$path = $this->normalizePath($path);
220
221
		switch ($mode) {
222
			case 'r':
223
			case 'rb':
224
				// Stat cache does not need to be cleared when opening in read mode
225
				$stat = $this->stat($path);
226
				if (\is_array($stat)) {
227
					try {
228
						return $this->objectStore->readObject($this->getURN($stat['fileid']));
229
					} catch (\Exception $ex) {
230
						\OCP\Util::writeLog('objectstore', 'Could not get object: ' . $ex->getMessage(), \OCP\Util::ERROR);
231
						return false;
232
					}
233
				} else {
234
					return false;
235
				}
236
				// no break
237
			case 'w':
238
			case 'wb':
239
			case 'a':
240
			case 'ab':
241
			case 'r+':
242
			case 'w+':
243
			case 'wb+':
244
			case 'a+':
245
			case 'x':
246
			case 'x+':
247
			case 'c':
248
			case 'c+':
249
				// In write mode, invalidate stat cache
250
				$this->removeObjectStat($path);
251
				if (\strrpos($path, '.') !== false) {
252
					$ext = \substr($path, \strrpos($path, '.'));
253
				} else {
254
					$ext = '';
255
				}
256
				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
257
				\OC\Files\Stream\Close::registerCallback($tmpFile, [$this, 'writeBack']);
258
				if ($this->file_exists($path)) {
259
					$source = $this->fopen($path, 'r');
260
					\file_put_contents($tmpFile, $source);
261
				}
262
				self::$tmpFiles[$tmpFile] = $path;
263
264
				return \fopen('close://' . $tmpFile, $mode);
265
		}
266
		return false;
267
	}
268
269
	/** {@inheritdoc} */
270
	public function file_exists($path) {
271
		$path = $this->normalizePath($path);
272
		return (bool)$this->stat($path);
273
	}
274
275
	/** {@inheritdoc} */
276
	public function rename($source, $target) {
277
		$source = $this->normalizePath($source);
278
		$target = $this->normalizePath($target);
279
280
		// Invalidate stat cache for both source and target
281
		$this->clearPathStat($source);
282
		$this->clearPathStat($target);
283
284
		// Rename file/folder
285
		$this->remove($target);
286
		$this->getCache()->move($source, $target);
287
		$this->touch(\dirname($target));
288
		return true;
289
	}
290
291
	/** {@inheritdoc} */
292
	public function copy($source, $target) {
293
		$source = $this->normalizePath($source);
294
		$target = $this->normalizePath($target);
295
296
		// Ensure that target stat cache is invalidated
297
		$this->clearPathStat($target);
298
299
		// Copy file/folder
300
		return parent::copy($source, $target);
301
	}
302
303
	/** {@inheritdoc} */
304
	public function getMimeType($path) {
305
		$path = $this->normalizePath($path);
306
		$stat = $this->stat($path);
307
		if (\is_array($stat)) {
308
			return $stat['mimetype'];
309
		} else {
310
			return false;
311
		}
312
	}
313
314
	/** {@inheritdoc} */
315
	public function touch($path, $mtime = null) {
316
		$path = $this->normalizePath($path);
317
318
		$dirName = \dirname($path);
319
		$parentExists = $this->is_dir($dirName);
320
		if (!$parentExists) {
321
			return false;
322
		}
323
324
		// Get new mtime if not specified
325
		if ($mtime === null) {
326
			$mtime = \time();
327
		}
328
329
		$stat = $this->stat($path);
330
		if (\is_array($stat)) {
331
			// Remove stat cache before updating file
332
			$this->removeObjectStat($path);
333
334
			// update existing mtime in db
335
			$stat['mtime'] = $mtime;
336
			$this->getCache()->update($stat['fileid'], $stat);
337
		} else {
338
			// create new file
339
			$stat = [
340
				'etag' => $this->getETag($path),
341
				'mimetype' => \OC::$server->getMimeTypeDetector()->detectPath($path),
342
				'size' => 0,
343
				'mtime' => $mtime,
344
				'storage_mtime' => $mtime,
345
				'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
346
			];
347
			$fileId = $this->getCache()->put($path, $stat);
348
			try {
349
				//read an empty file from memory
350
				$this->objectStore->writeObject($this->getURN($fileId), \fopen('php://memory', 'r'));
351
			} catch (\Exception $ex) {
352
				$this->getCache()->remove($path);
353
				\OCP\Util::writeLog('objectstore', 'Could not create object: ' . $ex->getMessage(), \OCP\Util::ERROR);
354
				return false;
355
			}
356
		}
357
		return true;
358
	}
359
360
	/** {@inheritdoc} */
361
	public function writeBack($tmpFile) {
362
		if (!isset(self::$tmpFiles[$tmpFile])) {
363
			return;
364
		}
365
366
		$path = self::$tmpFiles[$tmpFile];
367
		$stat = $this->stat($path);
368
		if (empty($stat)) {
369
			// create new file
370
			$stat = [
371
				'permissions' => \OCP\Constants::PERMISSION_ALL - \OCP\Constants::PERMISSION_CREATE,
372
			];
373
		}
374
		// update stat with new data
375
		$mTime = \time();
376
		$stat['size'] = \filesize($tmpFile);
377
		$stat['mtime'] = $mTime;
378
		$stat['storage_mtime'] = $mTime;
379
		$stat['mimetype'] = \OC::$server->getMimeTypeDetector()->detect($tmpFile);
380
		$stat['etag'] = $this->getETag($path);
381
382
		// Remove stat cache before writing to file
383
		$this->removeObjectStat($path);
384
385
		// Write object
386
		$fileId = $this->getCache()->put($path, $stat);
387
		try {
388
			//upload to object storage
389
			$this->objectStore->writeObject($this->getURN($fileId), \fopen($tmpFile, 'r'));
390
		} catch (\Exception $ex) {
391
			$this->getCache()->remove($path);
392
			\OCP\Util::writeLog('objectstore', 'Could not create object: ' . $ex->getMessage(), \OCP\Util::ERROR);
393
			throw $ex; // make this bubble up
394
		}
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
	/** {@inheritdoc} */
409 View Code Duplication
	public function saveVersion($internalPath) {
410
		if ($this->objectStore instanceof IVersionedObjectStorage) {
411
			$stat = $this->stat($internalPath);
412
			// There are cases in the current implementation where saveVersion
413
			// is called before the file was even written.
414
			// There is nothing to be done in this case.
415
			// We return true to not trigger the fallback implementation
416
			if ($stat === false) {
417
				return true;
418
			}
419
			return $this->objectStore->saveVersion($this->getURN($stat['fileid']));
420
		}
421
		return parent::saveVersion($internalPath);
422
	}
423
424
	/** {@inheritdoc} */
425
	public function getVersions($internalPath) {
426
		if ($this->objectStore instanceof IVersionedObjectStorage) {
427
			$stat = $this->stat($internalPath);
428
			if ($stat === false) {
429
				throw new NotFoundException();
430
			}
431
			$versions = $this->objectStore->getVersions($this->getURN($stat['fileid']));
432
			list($uid, $path) = $this->convertInternalPathToGlobalPath($internalPath);
433
			return \array_map(function (array $version) use ($uid, $path) {
434
				$version['path'] = $path;
435
				$version['owner'] = $uid;
436
				return $version;
437
			}, $versions);
438
		}
439
		return parent::getVersions($internalPath);
440
	}
441
442
	/** {@inheritdoc} */
443
	public function getVersion($internalPath, $versionId) {
444
		if ($this->objectStore instanceof IVersionedObjectStorage) {
445
			$stat = $this->stat($internalPath);
446
			if ($stat === false) {
447
				throw new NotFoundException();
448
			}
449
			$version = $this->objectStore->getVersion($this->getURN($stat['fileid']), $versionId);
450
			list($uid, $path) = $this->convertInternalPathToGlobalPath($internalPath);
451
			if (!empty($version)) {
452
				$version['path'] = $path;
453
				$version['owner'] = $uid;
454
			}
455
			return $version;
456
		}
457
		return parent::getVersion($internalPath, $versionId);
458
	}
459
460
	/** {@inheritdoc} */
461 View Code Duplication
	public function getContentOfVersion($internalPath, $versionId) {
462
		if ($this->objectStore instanceof IVersionedObjectStorage) {
463
			$stat = $this->stat($internalPath);
464
			if ($stat === false) {
465
				throw new NotFoundException();
466
			}
467
			return $this->objectStore->getContentOfVersion($this->getURN($stat['fileid']), $versionId);
468
		}
469
		return parent::getContentOfVersion($internalPath, $versionId);
470
	}
471
472
	/** {@inheritdoc} */
473 View Code Duplication
	public function restoreVersion($internalPath, $versionId) {
474
		if ($this->objectStore instanceof IVersionedObjectStorage) {
475
			$stat = $this->stat($internalPath);
476
			if ($stat === false) {
477
				throw new NotFoundException();
478
			}
479
			return $this->objectStore->restoreVersion($this->getURN($stat['fileid']), $versionId);
480
		}
481
		return parent::restoreVersion($internalPath, $versionId);
482
	}
483
484
	/** {@inheritdoc} */
485
	public function copyFromStorage(\OCP\Files\Storage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
486
		return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime);
487
	}
488
489
	/** {@inheritdoc} */
490
	public function moveFromStorage(\OCP\Files\Storage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
491
		if ($sourceStorage === $this) {
492
			return $this->copy($sourceInternalPath, $targetInternalPath);
493
		}
494
		// cross storage moves need to perform a move operation
495
		// TODO: there is some cache updating missing which requires bigger changes and is
496
		//       subject to followup PRs
497
		if (!$sourceStorage->instanceOfStorage(self::class)) {
498
			return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
499
		}
500
501
		// source and target live on the same object store and we can simply rename
502
		// which updates the cache properly
503
		$this->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
504
		return true;
505
	}
506
507
	/**
508
	 * Object Stores use a NoopScanner because metadata is directly stored in
509
	 * the file cache and cannot really scan the filesystem. The storage passed in is not used anywhere.
510
	 *
511
	 * @param string $path
512
	 * @param \OC\Files\Storage\Storage (optional) the storage to pass to the scanner
513
	 * @return \OC\Files\ObjectStore\NoopScanner
514
	 */
515
	public function getScanner($path = '', $storage = null) {
516
		if (!$storage) {
517
			$storage = $this;
518
		}
519
		if (!isset($this->scanner)) {
520
			$this->scanner = new NoopScanner($storage);
521
		}
522
		return $this->scanner;
523
	}
524
525
	/**
526
	 * Override this method if you need a different unique resource identifier for your object storage implementation.
527
	 * The default implementations just appends the fileId to 'urn:oid:'. Make sure the URN is unique over all users.
528
	 * You may need a mapping table to store your URN if it cannot be generated from the fileid.
529
	 *
530
	 * @param int $fileId the fileid
531
	 * @return null|string the unified resource name used to identify the object
532
	 */
533
	protected function getURN($fileId) {
534
		if (\is_numeric($fileId)) {
535
			return $this->objectPrefix . $fileId;
536
		}
537
		return null;
538
	}
539
540
	private function rmObjects($path) {
541
		$children = $this->getCache()->getFolderContents($path);
542
		foreach ($children as $child) {
543
			if ($child['mimetype'] === 'httpd/unix-directory') {
544
				$this->rmObjects($child['path']);
545
			} else {
546
				$this->unlink($child['path']);
547
			}
548
		}
549
	}
550
551
	/**
552
	 * @param string $path
553
	 * @return string
554
	 */
555 View Code Duplication
	private function normalizePath($path) {
556
		$path = \trim($path, '/');
557
		//FIXME why do we sometimes get a path like 'files//username'?
558
		$path = \str_replace('//', '/', $path);
559
560
		// dirname('/folder') returns '.' but internally (in the cache) we store the root as ''
561
		if (!$path || $path === '.') {
562
			$path = '';
563
		}
564
565
		return $path;
566
	}
567
568
	/**
569
	 * Clear all object stat cache entries under this path
570
	 *
571
	 * @param $path
572
	 */
573
	private function clearPathStat($path) {
574
		$this->objectStatCache->clear($path);
575
	}
576
577
	/**
578
	 * Clear single object stat cache
579
	 *
580
	 * @param $path
581
	 */
582
	private function removeObjectStat($path) {
583
		$this->objectStatCache->remove($path);
584
	}
585
586
	/**
587
	 * Try to get object stat from cache, and return false if
588
	 * not existing. Filecache for folders will not be stored in
589
	 * the stat cache, and only objects are cached
590
	 *
591
	 * @param $path
592
	 * @return array|false
593
	 */
594
	private function getPathStat($path) {
595
		if ($this->objectStatCache->hasKey($path)) {
596
			return $this->objectStatCache->get($path);
597
		}
598
599
		$cacheEntry = $this->getCache()->get($path);
600
		if ($cacheEntry instanceof CacheEntry) {
601
			$stat = $cacheEntry->getData();
602
			if ($cacheEntry->getMimeType() != 'httpd/unix-directory') {
603
				// Only set stat cache for objects
604
				$this->objectStatCache->set($path, $stat);
605
			}
606
			return $stat;
607
		} else {
608
			return false;
609
		}
610
	}
611
}
612