Passed
Push — master ( 8fcc0e...98c8cd )
by Roeland
13:27 queued 11s
created
apps/files_external/lib/Lib/Storage/SMB.php 2 patches
Indentation   +659 added lines, -659 removed lines patch added patch discarded remove patch
@@ -68,663 +68,663 @@
 block discarded – undo
68 68
 use OCP\ILogger;
69 69
 
70 70
 class SMB extends Common implements INotifyStorage {
71
-	/**
72
-	 * @var \Icewind\SMB\IServer
73
-	 */
74
-	protected $server;
75
-
76
-	/**
77
-	 * @var \Icewind\SMB\IShare
78
-	 */
79
-	protected $share;
80
-
81
-	/**
82
-	 * @var string
83
-	 */
84
-	protected $root;
85
-
86
-	/**
87
-	 * @var \Icewind\SMB\IFileInfo[]
88
-	 */
89
-	protected $statCache;
90
-
91
-	/** @var ILogger */
92
-	protected $logger;
93
-
94
-	/** @var bool */
95
-	protected $showHidden;
96
-
97
-	/** @var bool */
98
-	protected $checkAcl;
99
-
100
-	public function __construct($params) {
101
-		if (!isset($params['host'])) {
102
-			throw new \Exception('Invalid configuration, no host provided');
103
-		}
104
-
105
-		if (isset($params['auth'])) {
106
-			$auth = $params['auth'];
107
-		} elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
108
-			[$workgroup, $user] = $this->splitUser($params['user']);
109
-			$auth = new BasicAuth($user, $workgroup, $params['password']);
110
-		} else {
111
-			throw new \Exception('Invalid configuration, no credentials provided');
112
-		}
113
-
114
-		if (isset($params['logger'])) {
115
-			$this->logger = $params['logger'];
116
-		} else {
117
-			$this->logger = \OC::$server->getLogger();
118
-		}
119
-
120
-		$options = new Options();
121
-		if (isset($params['timeout'])) {
122
-			$timeout = (int)$params['timeout'];
123
-			if ($timeout > 0) {
124
-				$options->setTimeout($timeout);
125
-			}
126
-		}
127
-		$serverFactory = new ServerFactory($options);
128
-		$this->server = $serverFactory->createServer($params['host'], $auth);
129
-		$this->share = $this->server->getShare(trim($params['share'], '/'));
130
-
131
-		$this->root = $params['root'] ?? '/';
132
-		$this->root = '/' . ltrim($this->root, '/');
133
-		$this->root = rtrim($this->root, '/') . '/';
134
-
135
-		$this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
136
-		$this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
137
-
138
-		$this->statCache = new CappedMemoryCache();
139
-		parent::__construct($params);
140
-	}
141
-
142
-	private function splitUser($user) {
143
-		if (strpos($user, '/')) {
144
-			return explode('/', $user, 2);
145
-		} elseif (strpos($user, '\\')) {
146
-			return explode('\\', $user);
147
-		} else {
148
-			return [null, $user];
149
-		}
150
-	}
151
-
152
-	/**
153
-	 * @return string
154
-	 */
155
-	public function getId() {
156
-		// FIXME: double slash to keep compatible with the old storage ids,
157
-		// failure to do so will lead to creation of a new storage id and
158
-		// loss of shares from the storage
159
-		return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
160
-	}
161
-
162
-	/**
163
-	 * @param string $path
164
-	 * @return string
165
-	 */
166
-	protected function buildPath($path) {
167
-		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
168
-	}
169
-
170
-	protected function relativePath($fullPath) {
171
-		if ($fullPath === $this->root) {
172
-			return '';
173
-		} elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
174
-			return substr($fullPath, strlen($this->root));
175
-		} else {
176
-			return null;
177
-		}
178
-	}
179
-
180
-	/**
181
-	 * @param string $path
182
-	 * @return \Icewind\SMB\IFileInfo
183
-	 * @throws StorageAuthException
184
-	 */
185
-	protected function getFileInfo($path) {
186
-		try {
187
-			$path = $this->buildPath($path);
188
-			if (!isset($this->statCache[$path])) {
189
-				$this->statCache[$path] = $this->share->stat($path);
190
-			}
191
-			return $this->statCache[$path];
192
-		} catch (ConnectException $e) {
193
-			$this->throwUnavailable($e);
194
-		} catch (ForbiddenException $e) {
195
-			// with php-smbclient, this exceptions is thrown when the provided password is invalid.
196
-			// Possible is also ForbiddenException with a different error code, so we check it.
197
-			if ($e->getCode() === 1) {
198
-				$this->throwUnavailable($e);
199
-			}
200
-			throw $e;
201
-		}
202
-	}
203
-
204
-	/**
205
-	 * @param \Exception $e
206
-	 * @throws StorageAuthException
207
-	 */
208
-	protected function throwUnavailable(\Exception $e) {
209
-		$this->logger->logException($e, ['message' => 'Error while getting file info']);
210
-		throw new StorageAuthException($e->getMessage(), $e);
211
-	}
212
-
213
-	/**
214
-	 * get the acl from fileinfo that is relevant for the configured user
215
-	 *
216
-	 * @param IFileInfo $file
217
-	 * @return ACL|null
218
-	 */
219
-	private function getACL(IFileInfo $file): ?ACL {
220
-		$acls = $file->getAcls();
221
-		foreach ($acls as $user => $acl) {
222
-			[, $user] = explode('\\', $user); // strip domain
223
-			if ($user === $this->server->getAuth()->getUsername()) {
224
-				return $acl;
225
-			}
226
-		}
227
-
228
-		return null;
229
-	}
230
-
231
-	/**
232
-	 * @param string $path
233
-	 * @return \Icewind\SMB\IFileInfo[]
234
-	 * @throws StorageNotAvailableException
235
-	 */
236
-	protected function getFolderContents($path): iterable {
237
-		try {
238
-			$path = ltrim($this->buildPath($path), '/');
239
-			try {
240
-				$files = $this->share->dir($path);
241
-			} catch (ForbiddenException $e) {
242
-				throw new NotPermittedException();
243
-			}
244
-			foreach ($files as $file) {
245
-				$this->statCache[$path . '/' . $file->getName()] = $file;
246
-			}
247
-
248
-			foreach ($files as $file) {
249
-				try {
250
-					// the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
251
-					// so we trigger the below exceptions where applicable
252
-					$hide = $file->isHidden() && !$this->showHidden;
253
-
254
-					if ($this->checkAcl && $acl = $this->getACL($file)) {
255
-						// if there is no explicit deny, we assume it's allowed
256
-						// this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
257
-						// additionally, it's better to have false negatives here then false positives
258
-						if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
259
-							$this->logger->debug('Hiding non readable entry ' . $file->getName());
260
-							return false;
261
-						}
262
-					}
263
-
264
-					if ($hide) {
265
-						$this->logger->debug('hiding hidden file ' . $file->getName());
266
-					}
267
-					if (!$hide) {
268
-						yield $file;
269
-					}
270
-				} catch (ForbiddenException $e) {
271
-					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);
272
-				} catch (NotFoundException $e) {
273
-					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);
274
-				}
275
-			}
276
-		} catch (ConnectException $e) {
277
-			$this->logger->logException($e, ['message' => 'Error while getting folder content']);
278
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
279
-		}
280
-	}
281
-
282
-	/**
283
-	 * @param \Icewind\SMB\IFileInfo $info
284
-	 * @return array
285
-	 */
286
-	protected function formatInfo($info) {
287
-		$result = [
288
-			'size' => $info->getSize(),
289
-			'mtime' => $info->getMTime(),
290
-		];
291
-		if ($info->isDirectory()) {
292
-			$result['type'] = 'dir';
293
-		} else {
294
-			$result['type'] = 'file';
295
-		}
296
-		return $result;
297
-	}
298
-
299
-	/**
300
-	 * Rename the files. If the source or the target is the root, the rename won't happen.
301
-	 *
302
-	 * @param string $source the old name of the path
303
-	 * @param string $target the new name of the path
304
-	 * @return bool true if the rename is successful, false otherwise
305
-	 */
306
-	public function rename($source, $target, $retry = true) {
307
-		if ($this->isRootDir($source) || $this->isRootDir($target)) {
308
-			return false;
309
-		}
310
-
311
-		$absoluteSource = $this->buildPath($source);
312
-		$absoluteTarget = $this->buildPath($target);
313
-		try {
314
-			$result = $this->share->rename($absoluteSource, $absoluteTarget);
315
-		} catch (AlreadyExistsException $e) {
316
-			if ($retry) {
317
-				$this->remove($target);
318
-				$result = $this->share->rename($absoluteSource, $absoluteTarget, false);
319
-			} else {
320
-				$this->logger->logException($e, ['level' => ILogger::WARN]);
321
-				return false;
322
-			}
323
-		} catch (InvalidArgumentException $e) {
324
-			if ($retry) {
325
-				$this->remove($target);
326
-				$result = $this->share->rename($absoluteSource, $absoluteTarget, false);
327
-			} else {
328
-				$this->logger->logException($e, ['level' => ILogger::WARN]);
329
-				return false;
330
-			}
331
-		} catch (\Exception $e) {
332
-			$this->logger->logException($e, ['level' => ILogger::WARN]);
333
-			return false;
334
-		}
335
-		unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
336
-		return $result;
337
-	}
338
-
339
-	public function stat($path, $retry = true) {
340
-		try {
341
-			$result = $this->formatInfo($this->getFileInfo($path));
342
-		} catch (ForbiddenException $e) {
343
-			return false;
344
-		} catch (NotFoundException $e) {
345
-			return false;
346
-		} catch (TimedOutException $e) {
347
-			if ($retry) {
348
-				return $this->stat($path, false);
349
-			} else {
350
-				throw $e;
351
-			}
352
-		}
353
-		if ($this->remoteIsShare() && $this->isRootDir($path)) {
354
-			$result['mtime'] = $this->shareMTime();
355
-		}
356
-		return $result;
357
-	}
358
-
359
-	/**
360
-	 * get the best guess for the modification time of the share
361
-	 *
362
-	 * @return int
363
-	 */
364
-	private function shareMTime() {
365
-		$highestMTime = 0;
366
-		$files = $this->share->dir($this->root);
367
-		foreach ($files as $fileInfo) {
368
-			try {
369
-				if ($fileInfo->getMTime() > $highestMTime) {
370
-					$highestMTime = $fileInfo->getMTime();
371
-				}
372
-			} catch (NotFoundException $e) {
373
-				// Ignore this, can happen on unavailable DFS shares
374
-			} catch (ForbiddenException $e) {
375
-				// Ignore this too - it's a symlink
376
-			}
377
-		}
378
-		return $highestMTime;
379
-	}
380
-
381
-	/**
382
-	 * Check if the path is our root dir (not the smb one)
383
-	 *
384
-	 * @param string $path the path
385
-	 * @return bool
386
-	 */
387
-	private function isRootDir($path) {
388
-		return $path === '' || $path === '/' || $path === '.';
389
-	}
390
-
391
-	/**
392
-	 * Check if our root points to a smb share
393
-	 *
394
-	 * @return bool true if our root points to a share false otherwise
395
-	 */
396
-	private function remoteIsShare() {
397
-		return $this->share->getName() && (!$this->root || $this->root === '/');
398
-	}
399
-
400
-	/**
401
-	 * @param string $path
402
-	 * @return bool
403
-	 */
404
-	public function unlink($path) {
405
-		if ($this->isRootDir($path)) {
406
-			return false;
407
-		}
408
-
409
-		try {
410
-			if ($this->is_dir($path)) {
411
-				return $this->rmdir($path);
412
-			} else {
413
-				$path = $this->buildPath($path);
414
-				unset($this->statCache[$path]);
415
-				$this->share->del($path);
416
-				return true;
417
-			}
418
-		} catch (NotFoundException $e) {
419
-			return false;
420
-		} catch (ForbiddenException $e) {
421
-			return false;
422
-		} catch (ConnectException $e) {
423
-			$this->logger->logException($e, ['message' => 'Error while deleting file']);
424
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
425
-		}
426
-	}
427
-
428
-	/**
429
-	 * check if a file or folder has been updated since $time
430
-	 *
431
-	 * @param string $path
432
-	 * @param int $time
433
-	 * @return bool
434
-	 */
435
-	public function hasUpdated($path, $time) {
436
-		if (!$path and $this->root === '/') {
437
-			// mtime doesn't work for shares, but giving the nature of the backend,
438
-			// doing a full update is still just fast enough
439
-			return true;
440
-		} else {
441
-			$actualTime = $this->filemtime($path);
442
-			return $actualTime > $time;
443
-		}
444
-	}
445
-
446
-	/**
447
-	 * @param string $path
448
-	 * @param string $mode
449
-	 * @return resource|bool
450
-	 */
451
-	public function fopen($path, $mode) {
452
-		$fullPath = $this->buildPath($path);
453
-		try {
454
-			switch ($mode) {
455
-				case 'r':
456
-				case 'rb':
457
-					if (!$this->file_exists($path)) {
458
-						return false;
459
-					}
460
-					return $this->share->read($fullPath);
461
-				case 'w':
462
-				case 'wb':
463
-					$source = $this->share->write($fullPath);
464
-					return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
465
-						unset($this->statCache[$fullPath]);
466
-					});
467
-				case 'a':
468
-				case 'ab':
469
-				case 'r+':
470
-				case 'w+':
471
-				case 'wb+':
472
-				case 'a+':
473
-				case 'x':
474
-				case 'x+':
475
-				case 'c':
476
-				case 'c+':
477
-					//emulate these
478
-					if (strrpos($path, '.') !== false) {
479
-						$ext = substr($path, strrpos($path, '.'));
480
-					} else {
481
-						$ext = '';
482
-					}
483
-					if ($this->file_exists($path)) {
484
-						if (!$this->isUpdatable($path)) {
485
-							return false;
486
-						}
487
-						$tmpFile = $this->getCachedFile($path);
488
-					} else {
489
-						if (!$this->isCreatable(dirname($path))) {
490
-							return false;
491
-						}
492
-						$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
493
-					}
494
-					$source = fopen($tmpFile, $mode);
495
-					$share = $this->share;
496
-					return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
497
-						unset($this->statCache[$fullPath]);
498
-						$share->put($tmpFile, $fullPath);
499
-						unlink($tmpFile);
500
-					});
501
-			}
502
-			return false;
503
-		} catch (NotFoundException $e) {
504
-			return false;
505
-		} catch (ForbiddenException $e) {
506
-			return false;
507
-		} catch (OutOfSpaceException $e) {
508
-			throw new EntityTooLargeException("not enough available space to create file", 0, $e);
509
-		} catch (ConnectException $e) {
510
-			$this->logger->logException($e, ['message' => 'Error while opening file']);
511
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
512
-		}
513
-	}
514
-
515
-	public function rmdir($path) {
516
-		if ($this->isRootDir($path)) {
517
-			return false;
518
-		}
519
-
520
-		try {
521
-			$this->statCache = [];
522
-			$content = $this->share->dir($this->buildPath($path));
523
-			foreach ($content as $file) {
524
-				if ($file->isDirectory()) {
525
-					$this->rmdir($path . '/' . $file->getName());
526
-				} else {
527
-					$this->share->del($file->getPath());
528
-				}
529
-			}
530
-			$this->share->rmdir($this->buildPath($path));
531
-			return true;
532
-		} catch (NotFoundException $e) {
533
-			return false;
534
-		} catch (ForbiddenException $e) {
535
-			return false;
536
-		} catch (ConnectException $e) {
537
-			$this->logger->logException($e, ['message' => 'Error while removing folder']);
538
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
539
-		}
540
-	}
541
-
542
-	public function touch($path, $mtime = null) {
543
-		try {
544
-			if (!$this->file_exists($path)) {
545
-				$fh = $this->share->write($this->buildPath($path));
546
-				fclose($fh);
547
-				return true;
548
-			}
549
-			return false;
550
-		} catch (OutOfSpaceException $e) {
551
-			throw new EntityTooLargeException("not enough available space to create file", 0, $e);
552
-		} catch (ConnectException $e) {
553
-			$this->logger->logException($e, ['message' => 'Error while creating file']);
554
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
555
-		}
556
-	}
557
-
558
-	public function getMetaData($path) {
559
-		$fileInfo = $this->getFileInfo($path);
560
-		if (!$fileInfo) {
561
-			return null;
562
-		}
563
-
564
-		return $this->getMetaDataFromFileInfo($fileInfo);
565
-	}
566
-
567
-	private function getMetaDataFromFileInfo(IFileInfo $fileInfo) {
568
-		$permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
569
-
570
-		if (!$fileInfo->isReadOnly()) {
571
-			$permissions += Constants::PERMISSION_DELETE;
572
-			$permissions += Constants::PERMISSION_UPDATE;
573
-			if ($fileInfo->isDirectory()) {
574
-				$permissions += Constants::PERMISSION_CREATE;
575
-			}
576
-		}
577
-
578
-		$data = [];
579
-		if ($fileInfo->isDirectory()) {
580
-			$data['mimetype'] = 'httpd/unix-directory';
581
-		} else {
582
-			$data['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath());
583
-		}
584
-		$data['mtime'] = $fileInfo->getMTime();
585
-		if ($fileInfo->isDirectory()) {
586
-			$data['size'] = -1; //unknown
587
-		} else {
588
-			$data['size'] = $fileInfo->getSize();
589
-		}
590
-		$data['etag'] = $this->getETag($fileInfo->getPath());
591
-		$data['storage_mtime'] = $data['mtime'];
592
-		$data['permissions'] = $permissions;
593
-		$data['name'] = $fileInfo->getName();
594
-
595
-		return $data;
596
-	}
597
-
598
-	public function opendir($path) {
599
-		try {
600
-			$files = $this->getFolderContents($path);
601
-		} catch (NotFoundException $e) {
602
-			return false;
603
-		} catch (NotPermittedException $e) {
604
-			return false;
605
-		}
606
-		$names = array_map(function ($info) {
607
-			/** @var \Icewind\SMB\IFileInfo $info */
608
-			return $info->getName();
609
-		}, iterator_to_array($files));
610
-		return IteratorDirectory::wrap($names);
611
-	}
612
-
613
-	public function getDirectoryContent($directory): \Traversable {
614
-		$files = $this->getFolderContents($directory);
615
-		foreach ($files as $file) {
616
-			yield $this->getMetaDataFromFileInfo($file);
617
-		}
618
-	}
619
-
620
-	public function filetype($path) {
621
-		try {
622
-			return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
623
-		} catch (NotFoundException $e) {
624
-			return false;
625
-		} catch (ForbiddenException $e) {
626
-			return false;
627
-		}
628
-	}
629
-
630
-	public function mkdir($path) {
631
-		$path = $this->buildPath($path);
632
-		try {
633
-			$this->share->mkdir($path);
634
-			return true;
635
-		} catch (ConnectException $e) {
636
-			$this->logger->logException($e, ['message' => 'Error while creating folder']);
637
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
638
-		} catch (Exception $e) {
639
-			return false;
640
-		}
641
-	}
642
-
643
-	public function file_exists($path) {
644
-		try {
645
-			$this->getFileInfo($path);
646
-			return true;
647
-		} catch (NotFoundException $e) {
648
-			return false;
649
-		} catch (ForbiddenException $e) {
650
-			return false;
651
-		} catch (ConnectException $e) {
652
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
653
-		}
654
-	}
655
-
656
-	public function isReadable($path) {
657
-		try {
658
-			$info = $this->getFileInfo($path);
659
-			return $this->showHidden || !$info->isHidden();
660
-		} catch (NotFoundException $e) {
661
-			return false;
662
-		} catch (ForbiddenException $e) {
663
-			return false;
664
-		}
665
-	}
666
-
667
-	public function isUpdatable($path) {
668
-		try {
669
-			$info = $this->getFileInfo($path);
670
-			// following windows behaviour for read-only folders: they can be written into
671
-			// (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
672
-			return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $this->is_dir($path));
673
-		} catch (NotFoundException $e) {
674
-			return false;
675
-		} catch (ForbiddenException $e) {
676
-			return false;
677
-		}
678
-	}
679
-
680
-	public function isDeletable($path) {
681
-		try {
682
-			$info = $this->getFileInfo($path);
683
-			return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
684
-		} catch (NotFoundException $e) {
685
-			return false;
686
-		} catch (ForbiddenException $e) {
687
-			return false;
688
-		}
689
-	}
690
-
691
-	/**
692
-	 * check if smbclient is installed
693
-	 */
694
-	public static function checkDependencies() {
695
-		return (
696
-			(bool)\OC_Helper::findBinaryPath('smbclient')
697
-			|| NativeServer::available(new System())
698
-		) ? true : ['smbclient'];
699
-	}
700
-
701
-	/**
702
-	 * Test a storage for availability
703
-	 *
704
-	 * @return bool
705
-	 */
706
-	public function test() {
707
-		try {
708
-			return parent::test();
709
-		} catch (Exception $e) {
710
-			$this->logger->logException($e);
711
-			return false;
712
-		}
713
-	}
714
-
715
-	public function listen($path, callable $callback) {
716
-		$this->notify($path)->listen(function (IChange $change) use ($callback) {
717
-			if ($change instanceof IRenameChange) {
718
-				return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
719
-			} else {
720
-				return $callback($change->getType(), $change->getPath());
721
-			}
722
-		});
723
-	}
724
-
725
-	public function notify($path) {
726
-		$path = '/' . ltrim($path, '/');
727
-		$shareNotifyHandler = $this->share->notify($this->buildPath($path));
728
-		return new SMBNotifyHandler($shareNotifyHandler, $this->root);
729
-	}
71
+    /**
72
+     * @var \Icewind\SMB\IServer
73
+     */
74
+    protected $server;
75
+
76
+    /**
77
+     * @var \Icewind\SMB\IShare
78
+     */
79
+    protected $share;
80
+
81
+    /**
82
+     * @var string
83
+     */
84
+    protected $root;
85
+
86
+    /**
87
+     * @var \Icewind\SMB\IFileInfo[]
88
+     */
89
+    protected $statCache;
90
+
91
+    /** @var ILogger */
92
+    protected $logger;
93
+
94
+    /** @var bool */
95
+    protected $showHidden;
96
+
97
+    /** @var bool */
98
+    protected $checkAcl;
99
+
100
+    public function __construct($params) {
101
+        if (!isset($params['host'])) {
102
+            throw new \Exception('Invalid configuration, no host provided');
103
+        }
104
+
105
+        if (isset($params['auth'])) {
106
+            $auth = $params['auth'];
107
+        } elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
108
+            [$workgroup, $user] = $this->splitUser($params['user']);
109
+            $auth = new BasicAuth($user, $workgroup, $params['password']);
110
+        } else {
111
+            throw new \Exception('Invalid configuration, no credentials provided');
112
+        }
113
+
114
+        if (isset($params['logger'])) {
115
+            $this->logger = $params['logger'];
116
+        } else {
117
+            $this->logger = \OC::$server->getLogger();
118
+        }
119
+
120
+        $options = new Options();
121
+        if (isset($params['timeout'])) {
122
+            $timeout = (int)$params['timeout'];
123
+            if ($timeout > 0) {
124
+                $options->setTimeout($timeout);
125
+            }
126
+        }
127
+        $serverFactory = new ServerFactory($options);
128
+        $this->server = $serverFactory->createServer($params['host'], $auth);
129
+        $this->share = $this->server->getShare(trim($params['share'], '/'));
130
+
131
+        $this->root = $params['root'] ?? '/';
132
+        $this->root = '/' . ltrim($this->root, '/');
133
+        $this->root = rtrim($this->root, '/') . '/';
134
+
135
+        $this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
136
+        $this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
137
+
138
+        $this->statCache = new CappedMemoryCache();
139
+        parent::__construct($params);
140
+    }
141
+
142
+    private function splitUser($user) {
143
+        if (strpos($user, '/')) {
144
+            return explode('/', $user, 2);
145
+        } elseif (strpos($user, '\\')) {
146
+            return explode('\\', $user);
147
+        } else {
148
+            return [null, $user];
149
+        }
150
+    }
151
+
152
+    /**
153
+     * @return string
154
+     */
155
+    public function getId() {
156
+        // FIXME: double slash to keep compatible with the old storage ids,
157
+        // failure to do so will lead to creation of a new storage id and
158
+        // loss of shares from the storage
159
+        return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
160
+    }
161
+
162
+    /**
163
+     * @param string $path
164
+     * @return string
165
+     */
166
+    protected function buildPath($path) {
167
+        return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
168
+    }
169
+
170
+    protected function relativePath($fullPath) {
171
+        if ($fullPath === $this->root) {
172
+            return '';
173
+        } elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
174
+            return substr($fullPath, strlen($this->root));
175
+        } else {
176
+            return null;
177
+        }
178
+    }
179
+
180
+    /**
181
+     * @param string $path
182
+     * @return \Icewind\SMB\IFileInfo
183
+     * @throws StorageAuthException
184
+     */
185
+    protected function getFileInfo($path) {
186
+        try {
187
+            $path = $this->buildPath($path);
188
+            if (!isset($this->statCache[$path])) {
189
+                $this->statCache[$path] = $this->share->stat($path);
190
+            }
191
+            return $this->statCache[$path];
192
+        } catch (ConnectException $e) {
193
+            $this->throwUnavailable($e);
194
+        } catch (ForbiddenException $e) {
195
+            // with php-smbclient, this exceptions is thrown when the provided password is invalid.
196
+            // Possible is also ForbiddenException with a different error code, so we check it.
197
+            if ($e->getCode() === 1) {
198
+                $this->throwUnavailable($e);
199
+            }
200
+            throw $e;
201
+        }
202
+    }
203
+
204
+    /**
205
+     * @param \Exception $e
206
+     * @throws StorageAuthException
207
+     */
208
+    protected function throwUnavailable(\Exception $e) {
209
+        $this->logger->logException($e, ['message' => 'Error while getting file info']);
210
+        throw new StorageAuthException($e->getMessage(), $e);
211
+    }
212
+
213
+    /**
214
+     * get the acl from fileinfo that is relevant for the configured user
215
+     *
216
+     * @param IFileInfo $file
217
+     * @return ACL|null
218
+     */
219
+    private function getACL(IFileInfo $file): ?ACL {
220
+        $acls = $file->getAcls();
221
+        foreach ($acls as $user => $acl) {
222
+            [, $user] = explode('\\', $user); // strip domain
223
+            if ($user === $this->server->getAuth()->getUsername()) {
224
+                return $acl;
225
+            }
226
+        }
227
+
228
+        return null;
229
+    }
230
+
231
+    /**
232
+     * @param string $path
233
+     * @return \Icewind\SMB\IFileInfo[]
234
+     * @throws StorageNotAvailableException
235
+     */
236
+    protected function getFolderContents($path): iterable {
237
+        try {
238
+            $path = ltrim($this->buildPath($path), '/');
239
+            try {
240
+                $files = $this->share->dir($path);
241
+            } catch (ForbiddenException $e) {
242
+                throw new NotPermittedException();
243
+            }
244
+            foreach ($files as $file) {
245
+                $this->statCache[$path . '/' . $file->getName()] = $file;
246
+            }
247
+
248
+            foreach ($files as $file) {
249
+                try {
250
+                    // the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
251
+                    // so we trigger the below exceptions where applicable
252
+                    $hide = $file->isHidden() && !$this->showHidden;
253
+
254
+                    if ($this->checkAcl && $acl = $this->getACL($file)) {
255
+                        // if there is no explicit deny, we assume it's allowed
256
+                        // this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
257
+                        // additionally, it's better to have false negatives here then false positives
258
+                        if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
259
+                            $this->logger->debug('Hiding non readable entry ' . $file->getName());
260
+                            return false;
261
+                        }
262
+                    }
263
+
264
+                    if ($hide) {
265
+                        $this->logger->debug('hiding hidden file ' . $file->getName());
266
+                    }
267
+                    if (!$hide) {
268
+                        yield $file;
269
+                    }
270
+                } catch (ForbiddenException $e) {
271
+                    $this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);
272
+                } catch (NotFoundException $e) {
273
+                    $this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);
274
+                }
275
+            }
276
+        } catch (ConnectException $e) {
277
+            $this->logger->logException($e, ['message' => 'Error while getting folder content']);
278
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
279
+        }
280
+    }
281
+
282
+    /**
283
+     * @param \Icewind\SMB\IFileInfo $info
284
+     * @return array
285
+     */
286
+    protected function formatInfo($info) {
287
+        $result = [
288
+            'size' => $info->getSize(),
289
+            'mtime' => $info->getMTime(),
290
+        ];
291
+        if ($info->isDirectory()) {
292
+            $result['type'] = 'dir';
293
+        } else {
294
+            $result['type'] = 'file';
295
+        }
296
+        return $result;
297
+    }
298
+
299
+    /**
300
+     * Rename the files. If the source or the target is the root, the rename won't happen.
301
+     *
302
+     * @param string $source the old name of the path
303
+     * @param string $target the new name of the path
304
+     * @return bool true if the rename is successful, false otherwise
305
+     */
306
+    public function rename($source, $target, $retry = true) {
307
+        if ($this->isRootDir($source) || $this->isRootDir($target)) {
308
+            return false;
309
+        }
310
+
311
+        $absoluteSource = $this->buildPath($source);
312
+        $absoluteTarget = $this->buildPath($target);
313
+        try {
314
+            $result = $this->share->rename($absoluteSource, $absoluteTarget);
315
+        } catch (AlreadyExistsException $e) {
316
+            if ($retry) {
317
+                $this->remove($target);
318
+                $result = $this->share->rename($absoluteSource, $absoluteTarget, false);
319
+            } else {
320
+                $this->logger->logException($e, ['level' => ILogger::WARN]);
321
+                return false;
322
+            }
323
+        } catch (InvalidArgumentException $e) {
324
+            if ($retry) {
325
+                $this->remove($target);
326
+                $result = $this->share->rename($absoluteSource, $absoluteTarget, false);
327
+            } else {
328
+                $this->logger->logException($e, ['level' => ILogger::WARN]);
329
+                return false;
330
+            }
331
+        } catch (\Exception $e) {
332
+            $this->logger->logException($e, ['level' => ILogger::WARN]);
333
+            return false;
334
+        }
335
+        unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
336
+        return $result;
337
+    }
338
+
339
+    public function stat($path, $retry = true) {
340
+        try {
341
+            $result = $this->formatInfo($this->getFileInfo($path));
342
+        } catch (ForbiddenException $e) {
343
+            return false;
344
+        } catch (NotFoundException $e) {
345
+            return false;
346
+        } catch (TimedOutException $e) {
347
+            if ($retry) {
348
+                return $this->stat($path, false);
349
+            } else {
350
+                throw $e;
351
+            }
352
+        }
353
+        if ($this->remoteIsShare() && $this->isRootDir($path)) {
354
+            $result['mtime'] = $this->shareMTime();
355
+        }
356
+        return $result;
357
+    }
358
+
359
+    /**
360
+     * get the best guess for the modification time of the share
361
+     *
362
+     * @return int
363
+     */
364
+    private function shareMTime() {
365
+        $highestMTime = 0;
366
+        $files = $this->share->dir($this->root);
367
+        foreach ($files as $fileInfo) {
368
+            try {
369
+                if ($fileInfo->getMTime() > $highestMTime) {
370
+                    $highestMTime = $fileInfo->getMTime();
371
+                }
372
+            } catch (NotFoundException $e) {
373
+                // Ignore this, can happen on unavailable DFS shares
374
+            } catch (ForbiddenException $e) {
375
+                // Ignore this too - it's a symlink
376
+            }
377
+        }
378
+        return $highestMTime;
379
+    }
380
+
381
+    /**
382
+     * Check if the path is our root dir (not the smb one)
383
+     *
384
+     * @param string $path the path
385
+     * @return bool
386
+     */
387
+    private function isRootDir($path) {
388
+        return $path === '' || $path === '/' || $path === '.';
389
+    }
390
+
391
+    /**
392
+     * Check if our root points to a smb share
393
+     *
394
+     * @return bool true if our root points to a share false otherwise
395
+     */
396
+    private function remoteIsShare() {
397
+        return $this->share->getName() && (!$this->root || $this->root === '/');
398
+    }
399
+
400
+    /**
401
+     * @param string $path
402
+     * @return bool
403
+     */
404
+    public function unlink($path) {
405
+        if ($this->isRootDir($path)) {
406
+            return false;
407
+        }
408
+
409
+        try {
410
+            if ($this->is_dir($path)) {
411
+                return $this->rmdir($path);
412
+            } else {
413
+                $path = $this->buildPath($path);
414
+                unset($this->statCache[$path]);
415
+                $this->share->del($path);
416
+                return true;
417
+            }
418
+        } catch (NotFoundException $e) {
419
+            return false;
420
+        } catch (ForbiddenException $e) {
421
+            return false;
422
+        } catch (ConnectException $e) {
423
+            $this->logger->logException($e, ['message' => 'Error while deleting file']);
424
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
425
+        }
426
+    }
427
+
428
+    /**
429
+     * check if a file or folder has been updated since $time
430
+     *
431
+     * @param string $path
432
+     * @param int $time
433
+     * @return bool
434
+     */
435
+    public function hasUpdated($path, $time) {
436
+        if (!$path and $this->root === '/') {
437
+            // mtime doesn't work for shares, but giving the nature of the backend,
438
+            // doing a full update is still just fast enough
439
+            return true;
440
+        } else {
441
+            $actualTime = $this->filemtime($path);
442
+            return $actualTime > $time;
443
+        }
444
+    }
445
+
446
+    /**
447
+     * @param string $path
448
+     * @param string $mode
449
+     * @return resource|bool
450
+     */
451
+    public function fopen($path, $mode) {
452
+        $fullPath = $this->buildPath($path);
453
+        try {
454
+            switch ($mode) {
455
+                case 'r':
456
+                case 'rb':
457
+                    if (!$this->file_exists($path)) {
458
+                        return false;
459
+                    }
460
+                    return $this->share->read($fullPath);
461
+                case 'w':
462
+                case 'wb':
463
+                    $source = $this->share->write($fullPath);
464
+                    return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
465
+                        unset($this->statCache[$fullPath]);
466
+                    });
467
+                case 'a':
468
+                case 'ab':
469
+                case 'r+':
470
+                case 'w+':
471
+                case 'wb+':
472
+                case 'a+':
473
+                case 'x':
474
+                case 'x+':
475
+                case 'c':
476
+                case 'c+':
477
+                    //emulate these
478
+                    if (strrpos($path, '.') !== false) {
479
+                        $ext = substr($path, strrpos($path, '.'));
480
+                    } else {
481
+                        $ext = '';
482
+                    }
483
+                    if ($this->file_exists($path)) {
484
+                        if (!$this->isUpdatable($path)) {
485
+                            return false;
486
+                        }
487
+                        $tmpFile = $this->getCachedFile($path);
488
+                    } else {
489
+                        if (!$this->isCreatable(dirname($path))) {
490
+                            return false;
491
+                        }
492
+                        $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
493
+                    }
494
+                    $source = fopen($tmpFile, $mode);
495
+                    $share = $this->share;
496
+                    return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
497
+                        unset($this->statCache[$fullPath]);
498
+                        $share->put($tmpFile, $fullPath);
499
+                        unlink($tmpFile);
500
+                    });
501
+            }
502
+            return false;
503
+        } catch (NotFoundException $e) {
504
+            return false;
505
+        } catch (ForbiddenException $e) {
506
+            return false;
507
+        } catch (OutOfSpaceException $e) {
508
+            throw new EntityTooLargeException("not enough available space to create file", 0, $e);
509
+        } catch (ConnectException $e) {
510
+            $this->logger->logException($e, ['message' => 'Error while opening file']);
511
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
512
+        }
513
+    }
514
+
515
+    public function rmdir($path) {
516
+        if ($this->isRootDir($path)) {
517
+            return false;
518
+        }
519
+
520
+        try {
521
+            $this->statCache = [];
522
+            $content = $this->share->dir($this->buildPath($path));
523
+            foreach ($content as $file) {
524
+                if ($file->isDirectory()) {
525
+                    $this->rmdir($path . '/' . $file->getName());
526
+                } else {
527
+                    $this->share->del($file->getPath());
528
+                }
529
+            }
530
+            $this->share->rmdir($this->buildPath($path));
531
+            return true;
532
+        } catch (NotFoundException $e) {
533
+            return false;
534
+        } catch (ForbiddenException $e) {
535
+            return false;
536
+        } catch (ConnectException $e) {
537
+            $this->logger->logException($e, ['message' => 'Error while removing folder']);
538
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
539
+        }
540
+    }
541
+
542
+    public function touch($path, $mtime = null) {
543
+        try {
544
+            if (!$this->file_exists($path)) {
545
+                $fh = $this->share->write($this->buildPath($path));
546
+                fclose($fh);
547
+                return true;
548
+            }
549
+            return false;
550
+        } catch (OutOfSpaceException $e) {
551
+            throw new EntityTooLargeException("not enough available space to create file", 0, $e);
552
+        } catch (ConnectException $e) {
553
+            $this->logger->logException($e, ['message' => 'Error while creating file']);
554
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
555
+        }
556
+    }
557
+
558
+    public function getMetaData($path) {
559
+        $fileInfo = $this->getFileInfo($path);
560
+        if (!$fileInfo) {
561
+            return null;
562
+        }
563
+
564
+        return $this->getMetaDataFromFileInfo($fileInfo);
565
+    }
566
+
567
+    private function getMetaDataFromFileInfo(IFileInfo $fileInfo) {
568
+        $permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
569
+
570
+        if (!$fileInfo->isReadOnly()) {
571
+            $permissions += Constants::PERMISSION_DELETE;
572
+            $permissions += Constants::PERMISSION_UPDATE;
573
+            if ($fileInfo->isDirectory()) {
574
+                $permissions += Constants::PERMISSION_CREATE;
575
+            }
576
+        }
577
+
578
+        $data = [];
579
+        if ($fileInfo->isDirectory()) {
580
+            $data['mimetype'] = 'httpd/unix-directory';
581
+        } else {
582
+            $data['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath());
583
+        }
584
+        $data['mtime'] = $fileInfo->getMTime();
585
+        if ($fileInfo->isDirectory()) {
586
+            $data['size'] = -1; //unknown
587
+        } else {
588
+            $data['size'] = $fileInfo->getSize();
589
+        }
590
+        $data['etag'] = $this->getETag($fileInfo->getPath());
591
+        $data['storage_mtime'] = $data['mtime'];
592
+        $data['permissions'] = $permissions;
593
+        $data['name'] = $fileInfo->getName();
594
+
595
+        return $data;
596
+    }
597
+
598
+    public function opendir($path) {
599
+        try {
600
+            $files = $this->getFolderContents($path);
601
+        } catch (NotFoundException $e) {
602
+            return false;
603
+        } catch (NotPermittedException $e) {
604
+            return false;
605
+        }
606
+        $names = array_map(function ($info) {
607
+            /** @var \Icewind\SMB\IFileInfo $info */
608
+            return $info->getName();
609
+        }, iterator_to_array($files));
610
+        return IteratorDirectory::wrap($names);
611
+    }
612
+
613
+    public function getDirectoryContent($directory): \Traversable {
614
+        $files = $this->getFolderContents($directory);
615
+        foreach ($files as $file) {
616
+            yield $this->getMetaDataFromFileInfo($file);
617
+        }
618
+    }
619
+
620
+    public function filetype($path) {
621
+        try {
622
+            return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
623
+        } catch (NotFoundException $e) {
624
+            return false;
625
+        } catch (ForbiddenException $e) {
626
+            return false;
627
+        }
628
+    }
629
+
630
+    public function mkdir($path) {
631
+        $path = $this->buildPath($path);
632
+        try {
633
+            $this->share->mkdir($path);
634
+            return true;
635
+        } catch (ConnectException $e) {
636
+            $this->logger->logException($e, ['message' => 'Error while creating folder']);
637
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
638
+        } catch (Exception $e) {
639
+            return false;
640
+        }
641
+    }
642
+
643
+    public function file_exists($path) {
644
+        try {
645
+            $this->getFileInfo($path);
646
+            return true;
647
+        } catch (NotFoundException $e) {
648
+            return false;
649
+        } catch (ForbiddenException $e) {
650
+            return false;
651
+        } catch (ConnectException $e) {
652
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
653
+        }
654
+    }
655
+
656
+    public function isReadable($path) {
657
+        try {
658
+            $info = $this->getFileInfo($path);
659
+            return $this->showHidden || !$info->isHidden();
660
+        } catch (NotFoundException $e) {
661
+            return false;
662
+        } catch (ForbiddenException $e) {
663
+            return false;
664
+        }
665
+    }
666
+
667
+    public function isUpdatable($path) {
668
+        try {
669
+            $info = $this->getFileInfo($path);
670
+            // following windows behaviour for read-only folders: they can be written into
671
+            // (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
672
+            return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $this->is_dir($path));
673
+        } catch (NotFoundException $e) {
674
+            return false;
675
+        } catch (ForbiddenException $e) {
676
+            return false;
677
+        }
678
+    }
679
+
680
+    public function isDeletable($path) {
681
+        try {
682
+            $info = $this->getFileInfo($path);
683
+            return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
684
+        } catch (NotFoundException $e) {
685
+            return false;
686
+        } catch (ForbiddenException $e) {
687
+            return false;
688
+        }
689
+    }
690
+
691
+    /**
692
+     * check if smbclient is installed
693
+     */
694
+    public static function checkDependencies() {
695
+        return (
696
+            (bool)\OC_Helper::findBinaryPath('smbclient')
697
+            || NativeServer::available(new System())
698
+        ) ? true : ['smbclient'];
699
+    }
700
+
701
+    /**
702
+     * Test a storage for availability
703
+     *
704
+     * @return bool
705
+     */
706
+    public function test() {
707
+        try {
708
+            return parent::test();
709
+        } catch (Exception $e) {
710
+            $this->logger->logException($e);
711
+            return false;
712
+        }
713
+    }
714
+
715
+    public function listen($path, callable $callback) {
716
+        $this->notify($path)->listen(function (IChange $change) use ($callback) {
717
+            if ($change instanceof IRenameChange) {
718
+                return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
719
+            } else {
720
+                return $callback($change->getType(), $change->getPath());
721
+            }
722
+        });
723
+    }
724
+
725
+    public function notify($path) {
726
+        $path = '/' . ltrim($path, '/');
727
+        $shareNotifyHandler = $this->share->notify($this->buildPath($path));
728
+        return new SMBNotifyHandler($shareNotifyHandler, $this->root);
729
+    }
730 730
 }
Please login to merge, or discard this patch.
Spacing   +17 added lines, -17 removed lines patch added patch discarded remove patch
@@ -119,7 +119,7 @@  discard block
 block discarded – undo
119 119
 
120 120
 		$options = new Options();
121 121
 		if (isset($params['timeout'])) {
122
-			$timeout = (int)$params['timeout'];
122
+			$timeout = (int) $params['timeout'];
123 123
 			if ($timeout > 0) {
124 124
 				$options->setTimeout($timeout);
125 125
 			}
@@ -129,8 +129,8 @@  discard block
 block discarded – undo
129 129
 		$this->share = $this->server->getShare(trim($params['share'], '/'));
130 130
 
131 131
 		$this->root = $params['root'] ?? '/';
132
-		$this->root = '/' . ltrim($this->root, '/');
133
-		$this->root = rtrim($this->root, '/') . '/';
132
+		$this->root = '/'.ltrim($this->root, '/');
133
+		$this->root = rtrim($this->root, '/').'/';
134 134
 
135 135
 		$this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
136 136
 		$this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
@@ -156,7 +156,7 @@  discard block
 block discarded – undo
156 156
 		// FIXME: double slash to keep compatible with the old storage ids,
157 157
 		// failure to do so will lead to creation of a new storage id and
158 158
 		// loss of shares from the storage
159
-		return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
159
+		return 'smb::'.$this->server->getAuth()->getUsername().'@'.$this->server->getHost().'//'.$this->share->getName().'/'.$this->root;
160 160
 	}
161 161
 
162 162
 	/**
@@ -164,7 +164,7 @@  discard block
 block discarded – undo
164 164
 	 * @return string
165 165
 	 */
166 166
 	protected function buildPath($path) {
167
-		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
167
+		return Filesystem::normalizePath($this->root.'/'.$path, true, false, true);
168 168
 	}
169 169
 
170 170
 	protected function relativePath($fullPath) {
@@ -242,7 +242,7 @@  discard block
 block discarded – undo
242 242
 				throw new NotPermittedException();
243 243
 			}
244 244
 			foreach ($files as $file) {
245
-				$this->statCache[$path . '/' . $file->getName()] = $file;
245
+				$this->statCache[$path.'/'.$file->getName()] = $file;
246 246
 			}
247 247
 
248 248
 			foreach ($files as $file) {
@@ -256,21 +256,21 @@  discard block
 block discarded – undo
256 256
 						// this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
257 257
 						// additionally, it's better to have false negatives here then false positives
258 258
 						if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
259
-							$this->logger->debug('Hiding non readable entry ' . $file->getName());
259
+							$this->logger->debug('Hiding non readable entry '.$file->getName());
260 260
 							return false;
261 261
 						}
262 262
 					}
263 263
 
264 264
 					if ($hide) {
265
-						$this->logger->debug('hiding hidden file ' . $file->getName());
265
+						$this->logger->debug('hiding hidden file '.$file->getName());
266 266
 					}
267 267
 					if (!$hide) {
268 268
 						yield $file;
269 269
 					}
270 270
 				} catch (ForbiddenException $e) {
271
-					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);
271
+					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry '.$file->getName()]);
272 272
 				} catch (NotFoundException $e) {
273
-					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);
273
+					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry '.$file->getName()]);
274 274
 				}
275 275
 			}
276 276
 		} catch (ConnectException $e) {
@@ -461,7 +461,7 @@  discard block
 block discarded – undo
461 461
 				case 'w':
462 462
 				case 'wb':
463 463
 					$source = $this->share->write($fullPath);
464
-					return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
464
+					return CallBackWrapper::wrap($source, null, null, function() use ($fullPath) {
465 465
 						unset($this->statCache[$fullPath]);
466 466
 					});
467 467
 				case 'a':
@@ -493,7 +493,7 @@  discard block
 block discarded – undo
493 493
 					}
494 494
 					$source = fopen($tmpFile, $mode);
495 495
 					$share = $this->share;
496
-					return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
496
+					return CallbackWrapper::wrap($source, null, null, function() use ($tmpFile, $fullPath, $share) {
497 497
 						unset($this->statCache[$fullPath]);
498 498
 						$share->put($tmpFile, $fullPath);
499 499
 						unlink($tmpFile);
@@ -522,7 +522,7 @@  discard block
 block discarded – undo
522 522
 			$content = $this->share->dir($this->buildPath($path));
523 523
 			foreach ($content as $file) {
524 524
 				if ($file->isDirectory()) {
525
-					$this->rmdir($path . '/' . $file->getName());
525
+					$this->rmdir($path.'/'.$file->getName());
526 526
 				} else {
527 527
 					$this->share->del($file->getPath());
528 528
 				}
@@ -603,7 +603,7 @@  discard block
 block discarded – undo
603 603
 		} catch (NotPermittedException $e) {
604 604
 			return false;
605 605
 		}
606
-		$names = array_map(function ($info) {
606
+		$names = array_map(function($info) {
607 607
 			/** @var \Icewind\SMB\IFileInfo $info */
608 608
 			return $info->getName();
609 609
 		}, iterator_to_array($files));
@@ -693,7 +693,7 @@  discard block
 block discarded – undo
693 693
 	 */
694 694
 	public static function checkDependencies() {
695 695
 		return (
696
-			(bool)\OC_Helper::findBinaryPath('smbclient')
696
+			(bool) \OC_Helper::findBinaryPath('smbclient')
697 697
 			|| NativeServer::available(new System())
698 698
 		) ? true : ['smbclient'];
699 699
 	}
@@ -713,7 +713,7 @@  discard block
 block discarded – undo
713 713
 	}
714 714
 
715 715
 	public function listen($path, callable $callback) {
716
-		$this->notify($path)->listen(function (IChange $change) use ($callback) {
716
+		$this->notify($path)->listen(function(IChange $change) use ($callback) {
717 717
 			if ($change instanceof IRenameChange) {
718 718
 				return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
719 719
 			} else {
@@ -723,7 +723,7 @@  discard block
 block discarded – undo
723 723
 	}
724 724
 
725 725
 	public function notify($path) {
726
-		$path = '/' . ltrim($path, '/');
726
+		$path = '/'.ltrim($path, '/');
727 727
 		$shareNotifyHandler = $this->share->notify($this->buildPath($path));
728 728
 		return new SMBNotifyHandler($shareNotifyHandler, $this->root);
729 729
 	}
Please login to merge, or discard this patch.
apps/dav/lib/Connector/Sabre/Directory.php 1 patch
Indentation   +422 added lines, -422 removed lines patch added patch discarded remove patch
@@ -54,426 +54,426 @@
 block discarded – undo
54 54
 
55 55
 class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuota, \Sabre\DAV\IMoveTarget, \Sabre\DAV\ICopyTarget {
56 56
 
57
-	/**
58
-	 * Cached directory content
59
-	 *
60
-	 * @var \OCP\Files\FileInfo[]
61
-	 */
62
-	private $dirContent;
63
-
64
-	/**
65
-	 * Cached quota info
66
-	 *
67
-	 * @var array
68
-	 */
69
-	private $quotaInfo;
70
-
71
-	/**
72
-	 * @var ObjectTree|null
73
-	 */
74
-	private $tree;
75
-
76
-	/**
77
-	 * Sets up the node, expects a full path name
78
-	 *
79
-	 * @param \OC\Files\View $view
80
-	 * @param \OCP\Files\FileInfo $info
81
-	 * @param ObjectTree|null $tree
82
-	 * @param \OCP\Share\IManager $shareManager
83
-	 */
84
-	public function __construct(View $view, FileInfo $info, $tree = null, $shareManager = null) {
85
-		parent::__construct($view, $info, $shareManager);
86
-		$this->tree = $tree;
87
-	}
88
-
89
-	/**
90
-	 * Creates a new file in the directory
91
-	 *
92
-	 * Data will either be supplied as a stream resource, or in certain cases
93
-	 * as a string. Keep in mind that you may have to support either.
94
-	 *
95
-	 * After successful creation of the file, you may choose to return the ETag
96
-	 * of the new file here.
97
-	 *
98
-	 * The returned ETag must be surrounded by double-quotes (The quotes should
99
-	 * be part of the actual string).
100
-	 *
101
-	 * If you cannot accurately determine the ETag, you should not return it.
102
-	 * If you don't store the file exactly as-is (you're transforming it
103
-	 * somehow) you should also not return an ETag.
104
-	 *
105
-	 * This means that if a subsequent GET to this new file does not exactly
106
-	 * return the same contents of what was submitted here, you are strongly
107
-	 * recommended to omit the ETag.
108
-	 *
109
-	 * @param string $name Name of the file
110
-	 * @param resource|string $data Initial payload
111
-	 * @return null|string
112
-	 * @throws Exception\EntityTooLarge
113
-	 * @throws Exception\UnsupportedMediaType
114
-	 * @throws FileLocked
115
-	 * @throws InvalidPath
116
-	 * @throws \Sabre\DAV\Exception
117
-	 * @throws \Sabre\DAV\Exception\BadRequest
118
-	 * @throws \Sabre\DAV\Exception\Forbidden
119
-	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
120
-	 */
121
-	public function createFile($name, $data = null) {
122
-		try {
123
-			// for chunked upload also updating a existing file is a "createFile"
124
-			// because we create all the chunks before re-assemble them to the existing file.
125
-			if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
126
-
127
-				// exit if we can't create a new file and we don't updatable existing file
128
-				$chunkInfo = \OC_FileChunking::decodeName($name);
129
-				if (!$this->fileView->isCreatable($this->path) &&
130
-					!$this->fileView->isUpdatable($this->path . '/' . $chunkInfo['name'])
131
-				) {
132
-					throw new \Sabre\DAV\Exception\Forbidden();
133
-				}
134
-			} else {
135
-				// For non-chunked upload it is enough to check if we can create a new file
136
-				if (!$this->fileView->isCreatable($this->path)) {
137
-					throw new \Sabre\DAV\Exception\Forbidden();
138
-				}
139
-			}
140
-
141
-			$this->fileView->verifyPath($this->path, $name);
142
-
143
-			$path = $this->fileView->getAbsolutePath($this->path) . '/' . $name;
144
-			// in case the file already exists/overwriting
145
-			$info = $this->fileView->getFileInfo($this->path . '/' . $name);
146
-			if (!$info) {
147
-				// use a dummy FileInfo which is acceptable here since it will be refreshed after the put is complete
148
-				$info = new \OC\Files\FileInfo($path, null, null, [], null);
149
-			}
150
-			$node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info);
151
-
152
-			// only allow 1 process to upload a file at once but still allow reading the file while writing the part file
153
-			$node->acquireLock(ILockingProvider::LOCK_SHARED);
154
-			$this->fileView->lockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
155
-
156
-			$result = $node->put($data);
157
-
158
-			$this->fileView->unlockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
159
-			$node->releaseLock(ILockingProvider::LOCK_SHARED);
160
-			return $result;
161
-		} catch (\OCP\Files\StorageNotAvailableException $e) {
162
-			throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
163
-		} catch (InvalidPathException $ex) {
164
-			throw new InvalidPath($ex->getMessage(), false, $ex);
165
-		} catch (ForbiddenException $ex) {
166
-			throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
167
-		} catch (LockedException $e) {
168
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
169
-		}
170
-	}
171
-
172
-	/**
173
-	 * Creates a new subdirectory
174
-	 *
175
-	 * @param string $name
176
-	 * @throws FileLocked
177
-	 * @throws InvalidPath
178
-	 * @throws \Sabre\DAV\Exception\Forbidden
179
-	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
180
-	 */
181
-	public function createDirectory($name) {
182
-		try {
183
-			if (!$this->info->isCreatable()) {
184
-				throw new \Sabre\DAV\Exception\Forbidden();
185
-			}
186
-
187
-			$this->fileView->verifyPath($this->path, $name);
188
-			$newPath = $this->path . '/' . $name;
189
-			if (!$this->fileView->mkdir($newPath)) {
190
-				throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
191
-			}
192
-		} catch (\OCP\Files\StorageNotAvailableException $e) {
193
-			throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
194
-		} catch (InvalidPathException $ex) {
195
-			throw new InvalidPath($ex->getMessage());
196
-		} catch (ForbiddenException $ex) {
197
-			throw new Forbidden($ex->getMessage(), $ex->getRetry());
198
-		} catch (LockedException $e) {
199
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
200
-		}
201
-	}
202
-
203
-	/**
204
-	 * Returns a specific child node, referenced by its name
205
-	 *
206
-	 * @param string $name
207
-	 * @param \OCP\Files\FileInfo $info
208
-	 * @return \Sabre\DAV\INode
209
-	 * @throws InvalidPath
210
-	 * @throws \Sabre\DAV\Exception\NotFound
211
-	 * @throws \Sabre\DAV\Exception\ServiceUnavailable
212
-	 */
213
-	public function getChild($name, $info = null) {
214
-		if (!$this->info->isReadable()) {
215
-			// avoid detecting files through this way
216
-			throw new NotFound();
217
-		}
218
-
219
-		$path = $this->path . '/' . $name;
220
-		if (is_null($info)) {
221
-			try {
222
-				$this->fileView->verifyPath($this->path, $name);
223
-				$info = $this->fileView->getFileInfo($path);
224
-			} catch (\OCP\Files\StorageNotAvailableException $e) {
225
-				throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
226
-			} catch (InvalidPathException $ex) {
227
-				throw new InvalidPath($ex->getMessage());
228
-			} catch (ForbiddenException $e) {
229
-				throw new \Sabre\DAV\Exception\Forbidden();
230
-			}
231
-		}
232
-
233
-		if (!$info) {
234
-			throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
235
-		}
236
-
237
-		if ($info['mimetype'] === 'httpd/unix-directory') {
238
-			$node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
239
-		} else {
240
-			$node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info, $this->shareManager);
241
-		}
242
-		if ($this->tree) {
243
-			$this->tree->cacheNode($node);
244
-		}
245
-		return $node;
246
-	}
247
-
248
-	/**
249
-	 * Returns an array with all the child nodes
250
-	 *
251
-	 * @return \Sabre\DAV\INode[]
252
-	 * @throws \Sabre\DAV\Exception\Locked
253
-	 * @throws \OCA\DAV\Connector\Sabre\Exception\Forbidden
254
-	 */
255
-	public function getChildren() {
256
-		if (!is_null($this->dirContent)) {
257
-			return $this->dirContent;
258
-		}
259
-		try {
260
-			if (!$this->info->isReadable()) {
261
-				// return 403 instead of 404 because a 404 would make
262
-				// the caller believe that the collection itself does not exist
263
-				throw new Forbidden('No read permissions');
264
-			}
265
-			$folderContent = $this->fileView->getDirectoryContent($this->path);
266
-		} catch (LockedException $e) {
267
-			throw new Locked();
268
-		}
269
-
270
-		$nodes = [];
271
-		foreach ($folderContent as $info) {
272
-			$node = $this->getChild($info->getName(), $info);
273
-			$nodes[] = $node;
274
-		}
275
-		$this->dirContent = $nodes;
276
-		return $this->dirContent;
277
-	}
278
-
279
-	/**
280
-	 * Checks if a child exists.
281
-	 *
282
-	 * @param string $name
283
-	 * @return bool
284
-	 */
285
-	public function childExists($name) {
286
-		// note: here we do NOT resolve the chunk file name to the real file name
287
-		// to make sure we return false when checking for file existence with a chunk
288
-		// file name.
289
-		// This is to make sure that "createFile" is still triggered
290
-		// (required old code) instead of "updateFile".
291
-		//
292
-		// TODO: resolve chunk file name here and implement "updateFile"
293
-		$path = $this->path . '/' . $name;
294
-		return $this->fileView->file_exists($path);
295
-	}
296
-
297
-	/**
298
-	 * Deletes all files in this directory, and then itself
299
-	 *
300
-	 * @return void
301
-	 * @throws FileLocked
302
-	 * @throws \Sabre\DAV\Exception\Forbidden
303
-	 */
304
-	public function delete() {
305
-		if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) {
306
-			throw new \Sabre\DAV\Exception\Forbidden();
307
-		}
308
-
309
-		try {
310
-			if (!$this->fileView->rmdir($this->path)) {
311
-				// assume it wasn't possible to remove due to permission issue
312
-				throw new \Sabre\DAV\Exception\Forbidden();
313
-			}
314
-		} catch (ForbiddenException $ex) {
315
-			throw new Forbidden($ex->getMessage(), $ex->getRetry());
316
-		} catch (LockedException $e) {
317
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
318
-		}
319
-	}
320
-
321
-	/**
322
-	 * Returns available diskspace information
323
-	 *
324
-	 * @return array
325
-	 */
326
-	public function getQuotaInfo() {
327
-		if ($this->quotaInfo) {
328
-			return $this->quotaInfo;
329
-		}
330
-		try {
331
-			$info = $this->fileView->getFileInfo($this->path, false);
332
-			$storageInfo = \OC_Helper::getStorageInfo($this->info->getPath(), $info);
333
-			if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) {
334
-				$free = \OCP\Files\FileInfo::SPACE_UNLIMITED;
335
-			} else {
336
-				$free = $storageInfo['free'];
337
-			}
338
-			$this->quotaInfo = [
339
-				$storageInfo['used'],
340
-				$free
341
-			];
342
-			return $this->quotaInfo;
343
-		} catch (\OCP\Files\NotFoundException $e) {
344
-			return [0, 0];
345
-		} catch (\OCP\Files\StorageNotAvailableException $e) {
346
-			return [0, 0];
347
-		} catch (NotPermittedException $e) {
348
-			return [0, 0];
349
-		}
350
-	}
351
-
352
-	/**
353
-	 * Moves a node into this collection.
354
-	 *
355
-	 * It is up to the implementors to:
356
-	 *   1. Create the new resource.
357
-	 *   2. Remove the old resource.
358
-	 *   3. Transfer any properties or other data.
359
-	 *
360
-	 * Generally you should make very sure that your collection can easily move
361
-	 * the move.
362
-	 *
363
-	 * If you don't, just return false, which will trigger sabre/dav to handle
364
-	 * the move itself. If you return true from this function, the assumption
365
-	 * is that the move was successful.
366
-	 *
367
-	 * @param string $targetName New local file/collection name.
368
-	 * @param string $fullSourcePath Full path to source node
369
-	 * @param INode $sourceNode Source node itself
370
-	 * @return bool
371
-	 * @throws BadRequest
372
-	 * @throws ServiceUnavailable
373
-	 * @throws Forbidden
374
-	 * @throws FileLocked
375
-	 * @throws \Sabre\DAV\Exception\Forbidden
376
-	 */
377
-	public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
378
-		if (!$sourceNode instanceof Node) {
379
-			// it's a file of another kind, like FutureFile
380
-			if ($sourceNode instanceof IFile) {
381
-				// fallback to default copy+delete handling
382
-				return false;
383
-			}
384
-			throw new BadRequest('Incompatible node types');
385
-		}
386
-
387
-		if (!$this->fileView) {
388
-			throw new ServiceUnavailable('filesystem not setup');
389
-		}
390
-
391
-		$destinationPath = $this->getPath() . '/' . $targetName;
392
-
393
-
394
-		$targetNodeExists = $this->childExists($targetName);
395
-
396
-		// at getNodeForPath we also check the path for isForbiddenFileOrDir
397
-		// with that we have covered both source and destination
398
-		if ($sourceNode instanceof Directory && $targetNodeExists) {
399
-			throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists');
400
-		}
401
-
402
-		[$sourceDir,] = \Sabre\Uri\split($sourceNode->getPath());
403
-		$destinationDir = $this->getPath();
404
-
405
-		$sourcePath = $sourceNode->getPath();
406
-
407
-		$isMovableMount = false;
408
-		$sourceMount = \OC::$server->getMountManager()->find($this->fileView->getAbsolutePath($sourcePath));
409
-		$internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath));
410
-		if ($sourceMount instanceof MoveableMount && $internalPath === '') {
411
-			$isMovableMount = true;
412
-		}
413
-
414
-		try {
415
-			$sameFolder = ($sourceDir === $destinationDir);
416
-			// if we're overwriting or same folder
417
-			if ($targetNodeExists || $sameFolder) {
418
-				// note that renaming a share mount point is always allowed
419
-				if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) {
420
-					throw new \Sabre\DAV\Exception\Forbidden();
421
-				}
422
-			} else {
423
-				if (!$this->fileView->isCreatable($destinationDir)) {
424
-					throw new \Sabre\DAV\Exception\Forbidden();
425
-				}
426
-			}
427
-
428
-			if (!$sameFolder) {
429
-				// moving to a different folder, source will be gone, like a deletion
430
-				// note that moving a share mount point is always allowed
431
-				if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) {
432
-					throw new \Sabre\DAV\Exception\Forbidden();
433
-				}
434
-			}
435
-
436
-			$fileName = basename($destinationPath);
437
-			try {
438
-				$this->fileView->verifyPath($destinationDir, $fileName);
439
-			} catch (InvalidPathException $ex) {
440
-				throw new InvalidPath($ex->getMessage());
441
-			}
442
-
443
-			$renameOkay = $this->fileView->rename($sourcePath, $destinationPath);
444
-			if (!$renameOkay) {
445
-				throw new \Sabre\DAV\Exception\Forbidden('');
446
-			}
447
-		} catch (StorageNotAvailableException $e) {
448
-			throw new ServiceUnavailable($e->getMessage());
449
-		} catch (ForbiddenException $ex) {
450
-			throw new Forbidden($ex->getMessage(), $ex->getRetry());
451
-		} catch (LockedException $e) {
452
-			throw new FileLocked($e->getMessage(), $e->getCode(), $e);
453
-		}
454
-
455
-		return true;
456
-	}
457
-
458
-
459
-	public function copyInto($targetName, $sourcePath, INode $sourceNode) {
460
-		if ($sourceNode instanceof File || $sourceNode instanceof Directory) {
461
-			$destinationPath = $this->getPath() . '/' . $targetName;
462
-			$sourcePath = $sourceNode->getPath();
463
-
464
-			if (!$this->fileView->isCreatable($this->getPath())) {
465
-				throw new \Sabre\DAV\Exception\Forbidden();
466
-			}
467
-
468
-			try {
469
-				$this->fileView->verifyPath($this->getPath(), $targetName);
470
-			} catch (InvalidPathException $ex) {
471
-				throw new InvalidPath($ex->getMessage());
472
-			}
473
-
474
-			return $this->fileView->copy($sourcePath, $destinationPath);
475
-		}
476
-
477
-		return false;
478
-	}
57
+    /**
58
+     * Cached directory content
59
+     *
60
+     * @var \OCP\Files\FileInfo[]
61
+     */
62
+    private $dirContent;
63
+
64
+    /**
65
+     * Cached quota info
66
+     *
67
+     * @var array
68
+     */
69
+    private $quotaInfo;
70
+
71
+    /**
72
+     * @var ObjectTree|null
73
+     */
74
+    private $tree;
75
+
76
+    /**
77
+     * Sets up the node, expects a full path name
78
+     *
79
+     * @param \OC\Files\View $view
80
+     * @param \OCP\Files\FileInfo $info
81
+     * @param ObjectTree|null $tree
82
+     * @param \OCP\Share\IManager $shareManager
83
+     */
84
+    public function __construct(View $view, FileInfo $info, $tree = null, $shareManager = null) {
85
+        parent::__construct($view, $info, $shareManager);
86
+        $this->tree = $tree;
87
+    }
88
+
89
+    /**
90
+     * Creates a new file in the directory
91
+     *
92
+     * Data will either be supplied as a stream resource, or in certain cases
93
+     * as a string. Keep in mind that you may have to support either.
94
+     *
95
+     * After successful creation of the file, you may choose to return the ETag
96
+     * of the new file here.
97
+     *
98
+     * The returned ETag must be surrounded by double-quotes (The quotes should
99
+     * be part of the actual string).
100
+     *
101
+     * If you cannot accurately determine the ETag, you should not return it.
102
+     * If you don't store the file exactly as-is (you're transforming it
103
+     * somehow) you should also not return an ETag.
104
+     *
105
+     * This means that if a subsequent GET to this new file does not exactly
106
+     * return the same contents of what was submitted here, you are strongly
107
+     * recommended to omit the ETag.
108
+     *
109
+     * @param string $name Name of the file
110
+     * @param resource|string $data Initial payload
111
+     * @return null|string
112
+     * @throws Exception\EntityTooLarge
113
+     * @throws Exception\UnsupportedMediaType
114
+     * @throws FileLocked
115
+     * @throws InvalidPath
116
+     * @throws \Sabre\DAV\Exception
117
+     * @throws \Sabre\DAV\Exception\BadRequest
118
+     * @throws \Sabre\DAV\Exception\Forbidden
119
+     * @throws \Sabre\DAV\Exception\ServiceUnavailable
120
+     */
121
+    public function createFile($name, $data = null) {
122
+        try {
123
+            // for chunked upload also updating a existing file is a "createFile"
124
+            // because we create all the chunks before re-assemble them to the existing file.
125
+            if (isset($_SERVER['HTTP_OC_CHUNKED'])) {
126
+
127
+                // exit if we can't create a new file and we don't updatable existing file
128
+                $chunkInfo = \OC_FileChunking::decodeName($name);
129
+                if (!$this->fileView->isCreatable($this->path) &&
130
+                    !$this->fileView->isUpdatable($this->path . '/' . $chunkInfo['name'])
131
+                ) {
132
+                    throw new \Sabre\DAV\Exception\Forbidden();
133
+                }
134
+            } else {
135
+                // For non-chunked upload it is enough to check if we can create a new file
136
+                if (!$this->fileView->isCreatable($this->path)) {
137
+                    throw new \Sabre\DAV\Exception\Forbidden();
138
+                }
139
+            }
140
+
141
+            $this->fileView->verifyPath($this->path, $name);
142
+
143
+            $path = $this->fileView->getAbsolutePath($this->path) . '/' . $name;
144
+            // in case the file already exists/overwriting
145
+            $info = $this->fileView->getFileInfo($this->path . '/' . $name);
146
+            if (!$info) {
147
+                // use a dummy FileInfo which is acceptable here since it will be refreshed after the put is complete
148
+                $info = new \OC\Files\FileInfo($path, null, null, [], null);
149
+            }
150
+            $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info);
151
+
152
+            // only allow 1 process to upload a file at once but still allow reading the file while writing the part file
153
+            $node->acquireLock(ILockingProvider::LOCK_SHARED);
154
+            $this->fileView->lockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
155
+
156
+            $result = $node->put($data);
157
+
158
+            $this->fileView->unlockFile($path . '.upload.part', ILockingProvider::LOCK_EXCLUSIVE);
159
+            $node->releaseLock(ILockingProvider::LOCK_SHARED);
160
+            return $result;
161
+        } catch (\OCP\Files\StorageNotAvailableException $e) {
162
+            throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
163
+        } catch (InvalidPathException $ex) {
164
+            throw new InvalidPath($ex->getMessage(), false, $ex);
165
+        } catch (ForbiddenException $ex) {
166
+            throw new Forbidden($ex->getMessage(), $ex->getRetry(), $ex);
167
+        } catch (LockedException $e) {
168
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
169
+        }
170
+    }
171
+
172
+    /**
173
+     * Creates a new subdirectory
174
+     *
175
+     * @param string $name
176
+     * @throws FileLocked
177
+     * @throws InvalidPath
178
+     * @throws \Sabre\DAV\Exception\Forbidden
179
+     * @throws \Sabre\DAV\Exception\ServiceUnavailable
180
+     */
181
+    public function createDirectory($name) {
182
+        try {
183
+            if (!$this->info->isCreatable()) {
184
+                throw new \Sabre\DAV\Exception\Forbidden();
185
+            }
186
+
187
+            $this->fileView->verifyPath($this->path, $name);
188
+            $newPath = $this->path . '/' . $name;
189
+            if (!$this->fileView->mkdir($newPath)) {
190
+                throw new \Sabre\DAV\Exception\Forbidden('Could not create directory ' . $newPath);
191
+            }
192
+        } catch (\OCP\Files\StorageNotAvailableException $e) {
193
+            throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
194
+        } catch (InvalidPathException $ex) {
195
+            throw new InvalidPath($ex->getMessage());
196
+        } catch (ForbiddenException $ex) {
197
+            throw new Forbidden($ex->getMessage(), $ex->getRetry());
198
+        } catch (LockedException $e) {
199
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
200
+        }
201
+    }
202
+
203
+    /**
204
+     * Returns a specific child node, referenced by its name
205
+     *
206
+     * @param string $name
207
+     * @param \OCP\Files\FileInfo $info
208
+     * @return \Sabre\DAV\INode
209
+     * @throws InvalidPath
210
+     * @throws \Sabre\DAV\Exception\NotFound
211
+     * @throws \Sabre\DAV\Exception\ServiceUnavailable
212
+     */
213
+    public function getChild($name, $info = null) {
214
+        if (!$this->info->isReadable()) {
215
+            // avoid detecting files through this way
216
+            throw new NotFound();
217
+        }
218
+
219
+        $path = $this->path . '/' . $name;
220
+        if (is_null($info)) {
221
+            try {
222
+                $this->fileView->verifyPath($this->path, $name);
223
+                $info = $this->fileView->getFileInfo($path);
224
+            } catch (\OCP\Files\StorageNotAvailableException $e) {
225
+                throw new \Sabre\DAV\Exception\ServiceUnavailable($e->getMessage());
226
+            } catch (InvalidPathException $ex) {
227
+                throw new InvalidPath($ex->getMessage());
228
+            } catch (ForbiddenException $e) {
229
+                throw new \Sabre\DAV\Exception\Forbidden();
230
+            }
231
+        }
232
+
233
+        if (!$info) {
234
+            throw new \Sabre\DAV\Exception\NotFound('File with name ' . $path . ' could not be located');
235
+        }
236
+
237
+        if ($info['mimetype'] === 'httpd/unix-directory') {
238
+            $node = new \OCA\DAV\Connector\Sabre\Directory($this->fileView, $info, $this->tree, $this->shareManager);
239
+        } else {
240
+            $node = new \OCA\DAV\Connector\Sabre\File($this->fileView, $info, $this->shareManager);
241
+        }
242
+        if ($this->tree) {
243
+            $this->tree->cacheNode($node);
244
+        }
245
+        return $node;
246
+    }
247
+
248
+    /**
249
+     * Returns an array with all the child nodes
250
+     *
251
+     * @return \Sabre\DAV\INode[]
252
+     * @throws \Sabre\DAV\Exception\Locked
253
+     * @throws \OCA\DAV\Connector\Sabre\Exception\Forbidden
254
+     */
255
+    public function getChildren() {
256
+        if (!is_null($this->dirContent)) {
257
+            return $this->dirContent;
258
+        }
259
+        try {
260
+            if (!$this->info->isReadable()) {
261
+                // return 403 instead of 404 because a 404 would make
262
+                // the caller believe that the collection itself does not exist
263
+                throw new Forbidden('No read permissions');
264
+            }
265
+            $folderContent = $this->fileView->getDirectoryContent($this->path);
266
+        } catch (LockedException $e) {
267
+            throw new Locked();
268
+        }
269
+
270
+        $nodes = [];
271
+        foreach ($folderContent as $info) {
272
+            $node = $this->getChild($info->getName(), $info);
273
+            $nodes[] = $node;
274
+        }
275
+        $this->dirContent = $nodes;
276
+        return $this->dirContent;
277
+    }
278
+
279
+    /**
280
+     * Checks if a child exists.
281
+     *
282
+     * @param string $name
283
+     * @return bool
284
+     */
285
+    public function childExists($name) {
286
+        // note: here we do NOT resolve the chunk file name to the real file name
287
+        // to make sure we return false when checking for file existence with a chunk
288
+        // file name.
289
+        // This is to make sure that "createFile" is still triggered
290
+        // (required old code) instead of "updateFile".
291
+        //
292
+        // TODO: resolve chunk file name here and implement "updateFile"
293
+        $path = $this->path . '/' . $name;
294
+        return $this->fileView->file_exists($path);
295
+    }
296
+
297
+    /**
298
+     * Deletes all files in this directory, and then itself
299
+     *
300
+     * @return void
301
+     * @throws FileLocked
302
+     * @throws \Sabre\DAV\Exception\Forbidden
303
+     */
304
+    public function delete() {
305
+        if ($this->path === '' || $this->path === '/' || !$this->info->isDeletable()) {
306
+            throw new \Sabre\DAV\Exception\Forbidden();
307
+        }
308
+
309
+        try {
310
+            if (!$this->fileView->rmdir($this->path)) {
311
+                // assume it wasn't possible to remove due to permission issue
312
+                throw new \Sabre\DAV\Exception\Forbidden();
313
+            }
314
+        } catch (ForbiddenException $ex) {
315
+            throw new Forbidden($ex->getMessage(), $ex->getRetry());
316
+        } catch (LockedException $e) {
317
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
318
+        }
319
+    }
320
+
321
+    /**
322
+     * Returns available diskspace information
323
+     *
324
+     * @return array
325
+     */
326
+    public function getQuotaInfo() {
327
+        if ($this->quotaInfo) {
328
+            return $this->quotaInfo;
329
+        }
330
+        try {
331
+            $info = $this->fileView->getFileInfo($this->path, false);
332
+            $storageInfo = \OC_Helper::getStorageInfo($this->info->getPath(), $info);
333
+            if ($storageInfo['quota'] === \OCP\Files\FileInfo::SPACE_UNLIMITED) {
334
+                $free = \OCP\Files\FileInfo::SPACE_UNLIMITED;
335
+            } else {
336
+                $free = $storageInfo['free'];
337
+            }
338
+            $this->quotaInfo = [
339
+                $storageInfo['used'],
340
+                $free
341
+            ];
342
+            return $this->quotaInfo;
343
+        } catch (\OCP\Files\NotFoundException $e) {
344
+            return [0, 0];
345
+        } catch (\OCP\Files\StorageNotAvailableException $e) {
346
+            return [0, 0];
347
+        } catch (NotPermittedException $e) {
348
+            return [0, 0];
349
+        }
350
+    }
351
+
352
+    /**
353
+     * Moves a node into this collection.
354
+     *
355
+     * It is up to the implementors to:
356
+     *   1. Create the new resource.
357
+     *   2. Remove the old resource.
358
+     *   3. Transfer any properties or other data.
359
+     *
360
+     * Generally you should make very sure that your collection can easily move
361
+     * the move.
362
+     *
363
+     * If you don't, just return false, which will trigger sabre/dav to handle
364
+     * the move itself. If you return true from this function, the assumption
365
+     * is that the move was successful.
366
+     *
367
+     * @param string $targetName New local file/collection name.
368
+     * @param string $fullSourcePath Full path to source node
369
+     * @param INode $sourceNode Source node itself
370
+     * @return bool
371
+     * @throws BadRequest
372
+     * @throws ServiceUnavailable
373
+     * @throws Forbidden
374
+     * @throws FileLocked
375
+     * @throws \Sabre\DAV\Exception\Forbidden
376
+     */
377
+    public function moveInto($targetName, $fullSourcePath, INode $sourceNode) {
378
+        if (!$sourceNode instanceof Node) {
379
+            // it's a file of another kind, like FutureFile
380
+            if ($sourceNode instanceof IFile) {
381
+                // fallback to default copy+delete handling
382
+                return false;
383
+            }
384
+            throw new BadRequest('Incompatible node types');
385
+        }
386
+
387
+        if (!$this->fileView) {
388
+            throw new ServiceUnavailable('filesystem not setup');
389
+        }
390
+
391
+        $destinationPath = $this->getPath() . '/' . $targetName;
392
+
393
+
394
+        $targetNodeExists = $this->childExists($targetName);
395
+
396
+        // at getNodeForPath we also check the path for isForbiddenFileOrDir
397
+        // with that we have covered both source and destination
398
+        if ($sourceNode instanceof Directory && $targetNodeExists) {
399
+            throw new \Sabre\DAV\Exception\Forbidden('Could not copy directory ' . $sourceNode->getName() . ', target exists');
400
+        }
401
+
402
+        [$sourceDir,] = \Sabre\Uri\split($sourceNode->getPath());
403
+        $destinationDir = $this->getPath();
404
+
405
+        $sourcePath = $sourceNode->getPath();
406
+
407
+        $isMovableMount = false;
408
+        $sourceMount = \OC::$server->getMountManager()->find($this->fileView->getAbsolutePath($sourcePath));
409
+        $internalPath = $sourceMount->getInternalPath($this->fileView->getAbsolutePath($sourcePath));
410
+        if ($sourceMount instanceof MoveableMount && $internalPath === '') {
411
+            $isMovableMount = true;
412
+        }
413
+
414
+        try {
415
+            $sameFolder = ($sourceDir === $destinationDir);
416
+            // if we're overwriting or same folder
417
+            if ($targetNodeExists || $sameFolder) {
418
+                // note that renaming a share mount point is always allowed
419
+                if (!$this->fileView->isUpdatable($destinationDir) && !$isMovableMount) {
420
+                    throw new \Sabre\DAV\Exception\Forbidden();
421
+                }
422
+            } else {
423
+                if (!$this->fileView->isCreatable($destinationDir)) {
424
+                    throw new \Sabre\DAV\Exception\Forbidden();
425
+                }
426
+            }
427
+
428
+            if (!$sameFolder) {
429
+                // moving to a different folder, source will be gone, like a deletion
430
+                // note that moving a share mount point is always allowed
431
+                if (!$this->fileView->isDeletable($sourcePath) && !$isMovableMount) {
432
+                    throw new \Sabre\DAV\Exception\Forbidden();
433
+                }
434
+            }
435
+
436
+            $fileName = basename($destinationPath);
437
+            try {
438
+                $this->fileView->verifyPath($destinationDir, $fileName);
439
+            } catch (InvalidPathException $ex) {
440
+                throw new InvalidPath($ex->getMessage());
441
+            }
442
+
443
+            $renameOkay = $this->fileView->rename($sourcePath, $destinationPath);
444
+            if (!$renameOkay) {
445
+                throw new \Sabre\DAV\Exception\Forbidden('');
446
+            }
447
+        } catch (StorageNotAvailableException $e) {
448
+            throw new ServiceUnavailable($e->getMessage());
449
+        } catch (ForbiddenException $ex) {
450
+            throw new Forbidden($ex->getMessage(), $ex->getRetry());
451
+        } catch (LockedException $e) {
452
+            throw new FileLocked($e->getMessage(), $e->getCode(), $e);
453
+        }
454
+
455
+        return true;
456
+    }
457
+
458
+
459
+    public function copyInto($targetName, $sourcePath, INode $sourceNode) {
460
+        if ($sourceNode instanceof File || $sourceNode instanceof Directory) {
461
+            $destinationPath = $this->getPath() . '/' . $targetName;
462
+            $sourcePath = $sourceNode->getPath();
463
+
464
+            if (!$this->fileView->isCreatable($this->getPath())) {
465
+                throw new \Sabre\DAV\Exception\Forbidden();
466
+            }
467
+
468
+            try {
469
+                $this->fileView->verifyPath($this->getPath(), $targetName);
470
+            } catch (InvalidPathException $ex) {
471
+                throw new InvalidPath($ex->getMessage());
472
+            }
473
+
474
+            return $this->fileView->copy($sourcePath, $destinationPath);
475
+        }
476
+
477
+        return false;
478
+    }
479 479
 }
Please login to merge, or discard this patch.