Passed
Push — master ( e165dc...217243 )
by Roeland
12:21 queued 10s
created

ObjectStoreStorage::copyFile()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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