Completed
Push — master ( 4593b4...07bf00 )
by
unknown
26:10 queued 14s
created
apps/files_external/lib/Lib/Storage/SMB.php 2 patches
Indentation   +681 added lines, -681 removed lines patch added patch discarded remove patch
@@ -42,685 +42,685 @@
 block discarded – undo
42 42
 use Psr\Log\LoggerInterface;
43 43
 
44 44
 class SMB extends Common implements INotifyStorage {
45
-	/**
46
-	 * @var \Icewind\SMB\IServer
47
-	 */
48
-	protected $server;
49
-
50
-	/**
51
-	 * @var \Icewind\SMB\IShare
52
-	 */
53
-	protected $share;
54
-
55
-	/**
56
-	 * @var string
57
-	 */
58
-	protected $root;
59
-
60
-	/** @var CappedMemoryCache<IFileInfo> */
61
-	protected CappedMemoryCache $statCache;
62
-
63
-	/** @var LoggerInterface */
64
-	protected $logger;
65
-
66
-	/** @var bool */
67
-	protected $showHidden;
68
-
69
-	private bool $caseSensitive;
70
-
71
-	/** @var bool */
72
-	protected $checkAcl;
73
-
74
-	public function __construct(array $parameters) {
75
-		if (!isset($parameters['host'])) {
76
-			throw new \Exception('Invalid configuration, no host provided');
77
-		}
78
-
79
-		if (isset($parameters['auth'])) {
80
-			$auth = $parameters['auth'];
81
-		} elseif (isset($parameters['user']) && isset($parameters['password']) && isset($parameters['share'])) {
82
-			[$workgroup, $user] = $this->splitUser($parameters['user']);
83
-			$auth = new BasicAuth($user, $workgroup, $parameters['password']);
84
-		} else {
85
-			throw new \Exception('Invalid configuration, no credentials provided');
86
-		}
87
-
88
-		if (isset($parameters['logger'])) {
89
-			if (!$parameters['logger'] instanceof LoggerInterface) {
90
-				throw new \Exception(
91
-					'Invalid logger. Got '
92
-					. get_class($parameters['logger'])
93
-					. ' Expected ' . LoggerInterface::class
94
-				);
95
-			}
96
-			$this->logger = $parameters['logger'];
97
-		} else {
98
-			$this->logger = \OCP\Server::get(LoggerInterface::class);
99
-		}
100
-
101
-		$options = new Options();
102
-		if (isset($parameters['timeout'])) {
103
-			$timeout = (int)$parameters['timeout'];
104
-			if ($timeout > 0) {
105
-				$options->setTimeout($timeout);
106
-			}
107
-		}
108
-		$system = \OCP\Server::get(SystemBridge::class);
109
-		$serverFactory = new ServerFactory($options, $system);
110
-		$this->server = $serverFactory->createServer($parameters['host'], $auth);
111
-		$this->share = $this->server->getShare(trim($parameters['share'], '/'));
112
-
113
-		$this->root = $parameters['root'] ?? '/';
114
-		$this->root = '/' . ltrim($this->root, '/');
115
-		$this->root = rtrim($this->root, '/') . '/';
116
-
117
-		$this->showHidden = isset($parameters['show_hidden']) && $parameters['show_hidden'];
118
-		$this->caseSensitive = (bool)($parameters['case_sensitive'] ?? true);
119
-		$this->checkAcl = isset($parameters['check_acl']) && $parameters['check_acl'];
120
-
121
-		$this->statCache = new CappedMemoryCache();
122
-		parent::__construct($parameters);
123
-	}
124
-
125
-	private function splitUser(string $user): array {
126
-		if (str_contains($user, '/')) {
127
-			return explode('/', $user, 2);
128
-		} elseif (str_contains($user, '\\')) {
129
-			return explode('\\', $user);
130
-		}
131
-
132
-		return [null, $user];
133
-	}
134
-
135
-	public function getId(): string {
136
-		// FIXME: double slash to keep compatible with the old storage ids,
137
-		// failure to do so will lead to creation of a new storage id and
138
-		// loss of shares from the storage
139
-		return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
140
-	}
141
-
142
-	protected function buildPath(string $path): string {
143
-		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
144
-	}
145
-
146
-	protected function relativePath(string $fullPath): ?string {
147
-		if ($fullPath === $this->root) {
148
-			return '';
149
-		} elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
150
-			return substr($fullPath, strlen($this->root));
151
-		} else {
152
-			return null;
153
-		}
154
-	}
155
-
156
-	/**
157
-	 * @throws StorageAuthException
158
-	 * @throws \OCP\Files\NotFoundException
159
-	 * @throws \OCP\Files\ForbiddenException
160
-	 */
161
-	protected function getFileInfo(string $path): IFileInfo {
162
-		try {
163
-			$path = $this->buildPath($path);
164
-			$cached = $this->statCache[$path] ?? null;
165
-			if ($cached instanceof IFileInfo) {
166
-				return $cached;
167
-			} else {
168
-				$stat = $this->share->stat($path);
169
-				$this->statCache[$path] = $stat;
170
-				return $stat;
171
-			}
172
-		} catch (ConnectException $e) {
173
-			$this->throwUnavailable($e);
174
-		} catch (NotFoundException $e) {
175
-			throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
176
-		} catch (ForbiddenException $e) {
177
-			// with php-smbclient, this exception is thrown when the provided password is invalid.
178
-			// Possible is also ForbiddenException with a different error code, so we check it.
179
-			if ($e->getCode() === 1) {
180
-				$this->throwUnavailable($e);
181
-			}
182
-			throw new \OCP\Files\ForbiddenException($e->getMessage(), false, $e);
183
-		}
184
-	}
185
-
186
-	/**
187
-	 * @throws StorageAuthException
188
-	 */
189
-	protected function throwUnavailable(\Exception $e): never {
190
-		$this->logger->error('Error while getting file info', ['exception' => $e]);
191
-		throw new StorageAuthException($e->getMessage(), $e);
192
-	}
193
-
194
-	/**
195
-	 * get the acl from fileinfo that is relevant for the configured user
196
-	 */
197
-	private function getACL(IFileInfo $file): ?ACL {
198
-		try {
199
-			$acls = $file->getAcls();
200
-		} catch (Exception $e) {
201
-			$this->logger->warning('Error while getting file acls', ['exception' => $e]);
202
-			return null;
203
-		}
204
-		foreach ($acls as $user => $acl) {
205
-			[, $user] = $this->splitUser($user); // strip domain
206
-			if ($user === $this->server->getAuth()->getUsername()) {
207
-				return $acl;
208
-			}
209
-		}
210
-
211
-		return null;
212
-	}
213
-
214
-	/**
215
-	 * @return \Generator<IFileInfo>
216
-	 * @throws StorageNotAvailableException
217
-	 */
218
-	protected function getFolderContents(string $path): iterable {
219
-		try {
220
-			$path = ltrim($this->buildPath($path), '/');
221
-			try {
222
-				$files = $this->share->dir($path);
223
-			} catch (ForbiddenException $e) {
224
-				$this->logger->critical($e->getMessage(), ['exception' => $e]);
225
-				throw new NotPermittedException();
226
-			} catch (InvalidTypeException $e) {
227
-				return;
228
-			}
229
-			foreach ($files as $file) {
230
-				$this->statCache[$path . '/' . $file->getName()] = $file;
231
-			}
232
-
233
-			foreach ($files as $file) {
234
-				try {
235
-					// the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
236
-					// so we trigger the below exceptions where applicable
237
-					$hide = $file->isHidden() && !$this->showHidden;
238
-
239
-					if ($this->checkAcl && $acl = $this->getACL($file)) {
240
-						// if there is no explicit deny, we assume it's allowed
241
-						// this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
242
-						// additionally, it's better to have false negatives here then false positives
243
-						if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
244
-							$this->logger->debug('Hiding non readable entry ' . $file->getName());
245
-							continue;
246
-						}
247
-					}
248
-
249
-					if ($hide) {
250
-						$this->logger->debug('hiding hidden file ' . $file->getName());
251
-					}
252
-					if (!$hide) {
253
-						yield $file;
254
-					}
255
-				} catch (ForbiddenException $e) {
256
-					$this->logger->debug($e->getMessage(), ['exception' => $e]);
257
-				} catch (NotFoundException $e) {
258
-					$this->logger->debug('Hiding forbidden entry ' . $file->getName(), ['exception' => $e]);
259
-				}
260
-			}
261
-		} catch (ConnectException $e) {
262
-			$this->logger->error('Error while getting folder content', ['exception' => $e]);
263
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
264
-		} catch (NotFoundException $e) {
265
-			throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
266
-		}
267
-	}
268
-
269
-	protected function formatInfo(IFileInfo $info): array {
270
-		$result = [
271
-			'size' => $info->getSize(),
272
-			'mtime' => $info->getMTime(),
273
-		];
274
-		if ($info->isDirectory()) {
275
-			$result['type'] = 'dir';
276
-		} else {
277
-			$result['type'] = 'file';
278
-		}
279
-		return $result;
280
-	}
281
-
282
-	/**
283
-	 * Rename the files. If the source or the target is the root, the rename won't happen.
284
-	 *
285
-	 * @param string $source the old name of the path
286
-	 * @param string $target the new name of the path
287
-	 */
288
-	public function rename(string $source, string $target, bool $retry = true): bool {
289
-		if ($this->isRootDir($source) || $this->isRootDir($target)) {
290
-			return false;
291
-		}
292
-		if ($this->caseSensitive === false
293
-			&& mb_strtolower($target) === mb_strtolower($source)
294
-		) {
295
-			// Forbid changing case only on case-insensitive file system
296
-			return false;
297
-		}
298
-
299
-		$absoluteSource = $this->buildPath($source);
300
-		$absoluteTarget = $this->buildPath($target);
301
-		try {
302
-			$result = $this->share->rename($absoluteSource, $absoluteTarget);
303
-		} catch (AlreadyExistsException $e) {
304
-			if ($retry) {
305
-				$this->remove($target);
306
-				$result = $this->share->rename($absoluteSource, $absoluteTarget);
307
-			} else {
308
-				$this->logger->warning($e->getMessage(), ['exception' => $e]);
309
-				return false;
310
-			}
311
-		} catch (InvalidArgumentException $e) {
312
-			if ($retry) {
313
-				$this->remove($target);
314
-				$result = $this->share->rename($absoluteSource, $absoluteTarget);
315
-			} else {
316
-				$this->logger->warning($e->getMessage(), ['exception' => $e]);
317
-				return false;
318
-			}
319
-		} catch (\Exception $e) {
320
-			$this->logger->warning($e->getMessage(), ['exception' => $e]);
321
-			return false;
322
-		}
323
-		unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
324
-		return $result;
325
-	}
326
-
327
-	public function stat(string $path, bool $retry = true): array|false {
328
-		try {
329
-			$result = $this->formatInfo($this->getFileInfo($path));
330
-		} catch (\OCP\Files\ForbiddenException $e) {
331
-			return false;
332
-		} catch (\OCP\Files\NotFoundException $e) {
333
-			return false;
334
-		} catch (TimedOutException $e) {
335
-			if ($retry) {
336
-				return $this->stat($path, false);
337
-			} else {
338
-				throw $e;
339
-			}
340
-		}
341
-		if ($this->remoteIsShare() && $this->isRootDir($path)) {
342
-			$result['mtime'] = $this->shareMTime();
343
-		}
344
-		return $result;
345
-	}
346
-
347
-	/**
348
-	 * get the best guess for the modification time of the share
349
-	 */
350
-	private function shareMTime(): int {
351
-		$highestMTime = 0;
352
-		$files = $this->share->dir($this->root);
353
-		foreach ($files as $fileInfo) {
354
-			try {
355
-				if ($fileInfo->getMTime() > $highestMTime) {
356
-					$highestMTime = $fileInfo->getMTime();
357
-				}
358
-			} catch (NotFoundException $e) {
359
-				// Ignore this, can happen on unavailable DFS shares
360
-			} catch (ForbiddenException $e) {
361
-				// Ignore this too - it's a symlink
362
-			}
363
-		}
364
-		return $highestMTime;
365
-	}
366
-
367
-	/**
368
-	 * Check if the path is our root dir (not the smb one)
369
-	 */
370
-	private function isRootDir(string $path): bool {
371
-		return $path === '' || $path === '/' || $path === '.';
372
-	}
373
-
374
-	/**
375
-	 * Check if our root points to a smb share
376
-	 */
377
-	private function remoteIsShare(): bool {
378
-		return $this->share->getName() && (!$this->root || $this->root === '/');
379
-	}
380
-
381
-	public function unlink(string $path): bool {
382
-		if ($this->isRootDir($path)) {
383
-			return false;
384
-		}
385
-
386
-		try {
387
-			if ($this->is_dir($path)) {
388
-				return $this->rmdir($path);
389
-			} else {
390
-				$path = $this->buildPath($path);
391
-				unset($this->statCache[$path]);
392
-				$this->share->del($path);
393
-				return true;
394
-			}
395
-		} catch (NotFoundException $e) {
396
-			return false;
397
-		} catch (ForbiddenException $e) {
398
-			return false;
399
-		} catch (ConnectException $e) {
400
-			$this->logger->error('Error while deleting file', ['exception' => $e]);
401
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
402
-		}
403
-	}
404
-
405
-	/**
406
-	 * check if a file or folder has been updated since $time
407
-	 */
408
-	public function hasUpdated(string $path, int $time): bool {
409
-		if (!$path and $this->root === '/') {
410
-			// mtime doesn't work for shares, but giving the nature of the backend,
411
-			// doing a full update is still just fast enough
412
-			return true;
413
-		} else {
414
-			$actualTime = $this->filemtime($path);
415
-			return $actualTime > $time || $actualTime === 0;
416
-		}
417
-	}
418
-
419
-	/**
420
-	 * @return resource|false
421
-	 */
422
-	public function fopen(string $path, string $mode) {
423
-		$fullPath = $this->buildPath($path);
424
-		try {
425
-			switch ($mode) {
426
-				case 'r':
427
-				case 'rb':
428
-					if (!$this->file_exists($path)) {
429
-						$this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', file doesn\'t exist.');
430
-						return false;
431
-					}
432
-					return $this->share->read($fullPath);
433
-				case 'w':
434
-				case 'wb':
435
-					$source = $this->share->write($fullPath);
436
-					return CallBackWrapper::wrap($source, null, null, function () use ($fullPath): void {
437
-						unset($this->statCache[$fullPath]);
438
-					});
439
-				case 'a':
440
-				case 'ab':
441
-				case 'r+':
442
-				case 'w+':
443
-				case 'wb+':
444
-				case 'a+':
445
-				case 'x':
446
-				case 'x+':
447
-				case 'c':
448
-				case 'c+':
449
-					//emulate these
450
-					if (strrpos($path, '.') !== false) {
451
-						$ext = substr($path, strrpos($path, '.'));
452
-					} else {
453
-						$ext = '';
454
-					}
455
-					if ($this->file_exists($path)) {
456
-						if (!$this->isUpdatable($path)) {
457
-							$this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', file not updatable.');
458
-							return false;
459
-						}
460
-						$tmpFile = $this->getCachedFile($path);
461
-					} else {
462
-						if (!$this->isCreatable(dirname($path))) {
463
-							$this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', parent directory not writable.');
464
-							return false;
465
-						}
466
-						$tmpFile = \OCP\Server::get(ITempManager::class)->getTemporaryFile($ext);
467
-					}
468
-					$source = fopen($tmpFile, $mode);
469
-					$share = $this->share;
470
-					return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share): void {
471
-						unset($this->statCache[$fullPath]);
472
-						$share->put($tmpFile, $fullPath);
473
-						unlink($tmpFile);
474
-					});
475
-			}
476
-			return false;
477
-		} catch (NotFoundException $e) {
478
-			$this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', not found.', ['exception' => $e]);
479
-			return false;
480
-		} catch (ForbiddenException $e) {
481
-			$this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', forbidden.', ['exception' => $e]);
482
-			return false;
483
-		} catch (OutOfSpaceException $e) {
484
-			$this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', out of space.', ['exception' => $e]);
485
-			throw new EntityTooLargeException('not enough available space to create file', 0, $e);
486
-		} catch (ConnectException $e) {
487
-			$this->logger->error('Error while opening file ' . $path . ' on ' . $this->getId(), ['exception' => $e]);
488
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
489
-		}
490
-	}
491
-
492
-	public function rmdir(string $path): bool {
493
-		if ($this->isRootDir($path)) {
494
-			return false;
495
-		}
496
-
497
-		try {
498
-			$this->statCache = new CappedMemoryCache();
499
-			$content = $this->share->dir($this->buildPath($path));
500
-			foreach ($content as $file) {
501
-				if ($file->isDirectory()) {
502
-					$this->rmdir($path . '/' . $file->getName());
503
-				} else {
504
-					$this->share->del($file->getPath());
505
-				}
506
-			}
507
-			$this->share->rmdir($this->buildPath($path));
508
-			return true;
509
-		} catch (NotFoundException $e) {
510
-			return false;
511
-		} catch (ForbiddenException $e) {
512
-			return false;
513
-		} catch (ConnectException $e) {
514
-			$this->logger->error('Error while removing folder', ['exception' => $e]);
515
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
516
-		}
517
-	}
518
-
519
-	public function touch(string $path, ?int $mtime = null): bool {
520
-		try {
521
-			if (!$this->file_exists($path)) {
522
-				$fh = $this->share->write($this->buildPath($path));
523
-				fclose($fh);
524
-				return true;
525
-			}
526
-			return false;
527
-		} catch (OutOfSpaceException $e) {
528
-			throw new EntityTooLargeException('not enough available space to create file', 0, $e);
529
-		} catch (ConnectException $e) {
530
-			$this->logger->error('Error while creating file', ['exception' => $e]);
531
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
532
-		}
533
-	}
534
-
535
-	public function getMetaData(string $path): ?array {
536
-		try {
537
-			$fileInfo = $this->getFileInfo($path);
538
-		} catch (\OCP\Files\NotFoundException $e) {
539
-			return null;
540
-		} catch (\OCP\Files\ForbiddenException $e) {
541
-			return null;
542
-		}
543
-
544
-		return $this->getMetaDataFromFileInfo($fileInfo);
545
-	}
546
-
547
-	private function getMetaDataFromFileInfo(IFileInfo $fileInfo): array {
548
-		$permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
549
-
550
-		if (
551
-			!$fileInfo->isReadOnly() || $fileInfo->isDirectory()
552
-		) {
553
-			$permissions += Constants::PERMISSION_DELETE;
554
-			$permissions += Constants::PERMISSION_UPDATE;
555
-			if ($fileInfo->isDirectory()) {
556
-				$permissions += Constants::PERMISSION_CREATE;
557
-			}
558
-		}
559
-
560
-		$data = [];
561
-		if ($fileInfo->isDirectory()) {
562
-			$data['mimetype'] = 'httpd/unix-directory';
563
-		} else {
564
-			$data['mimetype'] = \OCP\Server::get(IMimeTypeDetector::class)->detectPath($fileInfo->getPath());
565
-		}
566
-		$data['mtime'] = $fileInfo->getMTime();
567
-		if ($fileInfo->isDirectory()) {
568
-			$data['size'] = -1; //unknown
569
-		} else {
570
-			$data['size'] = $fileInfo->getSize();
571
-		}
572
-		$data['etag'] = $this->getETag($fileInfo->getPath());
573
-		$data['storage_mtime'] = $data['mtime'];
574
-		$data['permissions'] = $permissions;
575
-		$data['name'] = $fileInfo->getName();
576
-
577
-		return $data;
578
-	}
579
-
580
-	public function opendir(string $path) {
581
-		try {
582
-			$files = $this->getFolderContents($path);
583
-		} catch (NotFoundException $e) {
584
-			return false;
585
-		} catch (NotPermittedException $e) {
586
-			return false;
587
-		}
588
-		$names = array_map(function ($info) {
589
-			/** @var IFileInfo $info */
590
-			return $info->getName();
591
-		}, iterator_to_array($files));
592
-		return IteratorDirectory::wrap($names);
593
-	}
594
-
595
-	public function getDirectoryContent(string $directory): \Traversable {
596
-		try {
597
-			$files = $this->getFolderContents($directory);
598
-			foreach ($files as $file) {
599
-				yield $this->getMetaDataFromFileInfo($file);
600
-			}
601
-		} catch (NotFoundException $e) {
602
-			return;
603
-		} catch (NotPermittedException $e) {
604
-			return;
605
-		}
606
-	}
607
-
608
-	public function filetype(string $path): string|false {
609
-		try {
610
-			return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
611
-		} catch (\OCP\Files\NotFoundException $e) {
612
-			return false;
613
-		} catch (\OCP\Files\ForbiddenException $e) {
614
-			return false;
615
-		}
616
-	}
617
-
618
-	public function mkdir(string $path): bool {
619
-		$path = $this->buildPath($path);
620
-		try {
621
-			$this->share->mkdir($path);
622
-			return true;
623
-		} catch (ConnectException $e) {
624
-			$this->logger->error('Error while creating folder', ['exception' => $e]);
625
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
626
-		} catch (Exception $e) {
627
-			return false;
628
-		}
629
-	}
630
-
631
-	public function file_exists(string $path): bool {
632
-		try {
633
-			// Case sensitive filesystem doesn't matter for root directory
634
-			if ($this->caseSensitive === false && $path !== '') {
635
-				$filename = basename($path);
636
-				$siblings = $this->getDirectoryContent(dirname($path));
637
-				foreach ($siblings as $sibling) {
638
-					if ($sibling['name'] === $filename) {
639
-						return true;
640
-					}
641
-				}
642
-				return false;
643
-			}
644
-			$this->getFileInfo($path);
645
-			return true;
646
-		} catch (\OCP\Files\NotFoundException $e) {
647
-			return false;
648
-		} catch (\OCP\Files\ForbiddenException $e) {
649
-			return false;
650
-		} catch (ConnectException $e) {
651
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
652
-		}
653
-	}
654
-
655
-	public function isReadable(string $path): bool {
656
-		try {
657
-			$info = $this->getFileInfo($path);
658
-			return $this->showHidden || !$info->isHidden();
659
-		} catch (\OCP\Files\NotFoundException $e) {
660
-			return false;
661
-		} catch (\OCP\Files\ForbiddenException $e) {
662
-			return false;
663
-		}
664
-	}
665
-
666
-	public function isUpdatable(string $path): bool {
667
-		try {
668
-			$info = $this->getFileInfo($path);
669
-			// following windows behaviour for read-only folders: they can be written into
670
-			// (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
671
-			return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory());
672
-		} catch (\OCP\Files\NotFoundException $e) {
673
-			return false;
674
-		} catch (\OCP\Files\ForbiddenException $e) {
675
-			return false;
676
-		}
677
-	}
678
-
679
-	public function isDeletable(string $path): bool {
680
-		try {
681
-			$info = $this->getFileInfo($path);
682
-			return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
683
-		} catch (\OCP\Files\NotFoundException $e) {
684
-			return false;
685
-		} catch (\OCP\Files\ForbiddenException $e) {
686
-			return false;
687
-		}
688
-	}
689
-
690
-	/**
691
-	 * check if smbclient is installed
692
-	 */
693
-	public static function checkDependencies(): array|bool {
694
-		$system = \OCP\Server::get(SystemBridge::class);
695
-		return Server::available($system) || NativeServer::available($system) ?: ['smbclient'];
696
-	}
697
-
698
-	public function test(): bool {
699
-		try {
700
-			return parent::test();
701
-		} catch (StorageAuthException $e) {
702
-			return false;
703
-		} catch (ForbiddenException $e) {
704
-			return false;
705
-		} catch (Exception $e) {
706
-			$this->logger->error($e->getMessage(), ['exception' => $e]);
707
-			return false;
708
-		}
709
-	}
710
-
711
-	public function listen(string $path, callable $callback): void {
712
-		$this->notify($path)->listen(function (IChange $change) use ($callback) {
713
-			if ($change instanceof IRenameChange) {
714
-				return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
715
-			} else {
716
-				return $callback($change->getType(), $change->getPath());
717
-			}
718
-		});
719
-	}
720
-
721
-	public function notify(string $path): SMBNotifyHandler {
722
-		$path = '/' . ltrim($path, '/');
723
-		$shareNotifyHandler = $this->share->notify($this->buildPath($path));
724
-		return new SMBNotifyHandler($shareNotifyHandler, $this->root);
725
-	}
45
+    /**
46
+     * @var \Icewind\SMB\IServer
47
+     */
48
+    protected $server;
49
+
50
+    /**
51
+     * @var \Icewind\SMB\IShare
52
+     */
53
+    protected $share;
54
+
55
+    /**
56
+     * @var string
57
+     */
58
+    protected $root;
59
+
60
+    /** @var CappedMemoryCache<IFileInfo> */
61
+    protected CappedMemoryCache $statCache;
62
+
63
+    /** @var LoggerInterface */
64
+    protected $logger;
65
+
66
+    /** @var bool */
67
+    protected $showHidden;
68
+
69
+    private bool $caseSensitive;
70
+
71
+    /** @var bool */
72
+    protected $checkAcl;
73
+
74
+    public function __construct(array $parameters) {
75
+        if (!isset($parameters['host'])) {
76
+            throw new \Exception('Invalid configuration, no host provided');
77
+        }
78
+
79
+        if (isset($parameters['auth'])) {
80
+            $auth = $parameters['auth'];
81
+        } elseif (isset($parameters['user']) && isset($parameters['password']) && isset($parameters['share'])) {
82
+            [$workgroup, $user] = $this->splitUser($parameters['user']);
83
+            $auth = new BasicAuth($user, $workgroup, $parameters['password']);
84
+        } else {
85
+            throw new \Exception('Invalid configuration, no credentials provided');
86
+        }
87
+
88
+        if (isset($parameters['logger'])) {
89
+            if (!$parameters['logger'] instanceof LoggerInterface) {
90
+                throw new \Exception(
91
+                    'Invalid logger. Got '
92
+                    . get_class($parameters['logger'])
93
+                    . ' Expected ' . LoggerInterface::class
94
+                );
95
+            }
96
+            $this->logger = $parameters['logger'];
97
+        } else {
98
+            $this->logger = \OCP\Server::get(LoggerInterface::class);
99
+        }
100
+
101
+        $options = new Options();
102
+        if (isset($parameters['timeout'])) {
103
+            $timeout = (int)$parameters['timeout'];
104
+            if ($timeout > 0) {
105
+                $options->setTimeout($timeout);
106
+            }
107
+        }
108
+        $system = \OCP\Server::get(SystemBridge::class);
109
+        $serverFactory = new ServerFactory($options, $system);
110
+        $this->server = $serverFactory->createServer($parameters['host'], $auth);
111
+        $this->share = $this->server->getShare(trim($parameters['share'], '/'));
112
+
113
+        $this->root = $parameters['root'] ?? '/';
114
+        $this->root = '/' . ltrim($this->root, '/');
115
+        $this->root = rtrim($this->root, '/') . '/';
116
+
117
+        $this->showHidden = isset($parameters['show_hidden']) && $parameters['show_hidden'];
118
+        $this->caseSensitive = (bool)($parameters['case_sensitive'] ?? true);
119
+        $this->checkAcl = isset($parameters['check_acl']) && $parameters['check_acl'];
120
+
121
+        $this->statCache = new CappedMemoryCache();
122
+        parent::__construct($parameters);
123
+    }
124
+
125
+    private function splitUser(string $user): array {
126
+        if (str_contains($user, '/')) {
127
+            return explode('/', $user, 2);
128
+        } elseif (str_contains($user, '\\')) {
129
+            return explode('\\', $user);
130
+        }
131
+
132
+        return [null, $user];
133
+    }
134
+
135
+    public function getId(): string {
136
+        // FIXME: double slash to keep compatible with the old storage ids,
137
+        // failure to do so will lead to creation of a new storage id and
138
+        // loss of shares from the storage
139
+        return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
140
+    }
141
+
142
+    protected function buildPath(string $path): string {
143
+        return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
144
+    }
145
+
146
+    protected function relativePath(string $fullPath): ?string {
147
+        if ($fullPath === $this->root) {
148
+            return '';
149
+        } elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
150
+            return substr($fullPath, strlen($this->root));
151
+        } else {
152
+            return null;
153
+        }
154
+    }
155
+
156
+    /**
157
+     * @throws StorageAuthException
158
+     * @throws \OCP\Files\NotFoundException
159
+     * @throws \OCP\Files\ForbiddenException
160
+     */
161
+    protected function getFileInfo(string $path): IFileInfo {
162
+        try {
163
+            $path = $this->buildPath($path);
164
+            $cached = $this->statCache[$path] ?? null;
165
+            if ($cached instanceof IFileInfo) {
166
+                return $cached;
167
+            } else {
168
+                $stat = $this->share->stat($path);
169
+                $this->statCache[$path] = $stat;
170
+                return $stat;
171
+            }
172
+        } catch (ConnectException $e) {
173
+            $this->throwUnavailable($e);
174
+        } catch (NotFoundException $e) {
175
+            throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
176
+        } catch (ForbiddenException $e) {
177
+            // with php-smbclient, this exception is thrown when the provided password is invalid.
178
+            // Possible is also ForbiddenException with a different error code, so we check it.
179
+            if ($e->getCode() === 1) {
180
+                $this->throwUnavailable($e);
181
+            }
182
+            throw new \OCP\Files\ForbiddenException($e->getMessage(), false, $e);
183
+        }
184
+    }
185
+
186
+    /**
187
+     * @throws StorageAuthException
188
+     */
189
+    protected function throwUnavailable(\Exception $e): never {
190
+        $this->logger->error('Error while getting file info', ['exception' => $e]);
191
+        throw new StorageAuthException($e->getMessage(), $e);
192
+    }
193
+
194
+    /**
195
+     * get the acl from fileinfo that is relevant for the configured user
196
+     */
197
+    private function getACL(IFileInfo $file): ?ACL {
198
+        try {
199
+            $acls = $file->getAcls();
200
+        } catch (Exception $e) {
201
+            $this->logger->warning('Error while getting file acls', ['exception' => $e]);
202
+            return null;
203
+        }
204
+        foreach ($acls as $user => $acl) {
205
+            [, $user] = $this->splitUser($user); // strip domain
206
+            if ($user === $this->server->getAuth()->getUsername()) {
207
+                return $acl;
208
+            }
209
+        }
210
+
211
+        return null;
212
+    }
213
+
214
+    /**
215
+     * @return \Generator<IFileInfo>
216
+     * @throws StorageNotAvailableException
217
+     */
218
+    protected function getFolderContents(string $path): iterable {
219
+        try {
220
+            $path = ltrim($this->buildPath($path), '/');
221
+            try {
222
+                $files = $this->share->dir($path);
223
+            } catch (ForbiddenException $e) {
224
+                $this->logger->critical($e->getMessage(), ['exception' => $e]);
225
+                throw new NotPermittedException();
226
+            } catch (InvalidTypeException $e) {
227
+                return;
228
+            }
229
+            foreach ($files as $file) {
230
+                $this->statCache[$path . '/' . $file->getName()] = $file;
231
+            }
232
+
233
+            foreach ($files as $file) {
234
+                try {
235
+                    // the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
236
+                    // so we trigger the below exceptions where applicable
237
+                    $hide = $file->isHidden() && !$this->showHidden;
238
+
239
+                    if ($this->checkAcl && $acl = $this->getACL($file)) {
240
+                        // if there is no explicit deny, we assume it's allowed
241
+                        // this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
242
+                        // additionally, it's better to have false negatives here then false positives
243
+                        if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
244
+                            $this->logger->debug('Hiding non readable entry ' . $file->getName());
245
+                            continue;
246
+                        }
247
+                    }
248
+
249
+                    if ($hide) {
250
+                        $this->logger->debug('hiding hidden file ' . $file->getName());
251
+                    }
252
+                    if (!$hide) {
253
+                        yield $file;
254
+                    }
255
+                } catch (ForbiddenException $e) {
256
+                    $this->logger->debug($e->getMessage(), ['exception' => $e]);
257
+                } catch (NotFoundException $e) {
258
+                    $this->logger->debug('Hiding forbidden entry ' . $file->getName(), ['exception' => $e]);
259
+                }
260
+            }
261
+        } catch (ConnectException $e) {
262
+            $this->logger->error('Error while getting folder content', ['exception' => $e]);
263
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
264
+        } catch (NotFoundException $e) {
265
+            throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
266
+        }
267
+    }
268
+
269
+    protected function formatInfo(IFileInfo $info): array {
270
+        $result = [
271
+            'size' => $info->getSize(),
272
+            'mtime' => $info->getMTime(),
273
+        ];
274
+        if ($info->isDirectory()) {
275
+            $result['type'] = 'dir';
276
+        } else {
277
+            $result['type'] = 'file';
278
+        }
279
+        return $result;
280
+    }
281
+
282
+    /**
283
+     * Rename the files. If the source or the target is the root, the rename won't happen.
284
+     *
285
+     * @param string $source the old name of the path
286
+     * @param string $target the new name of the path
287
+     */
288
+    public function rename(string $source, string $target, bool $retry = true): bool {
289
+        if ($this->isRootDir($source) || $this->isRootDir($target)) {
290
+            return false;
291
+        }
292
+        if ($this->caseSensitive === false
293
+            && mb_strtolower($target) === mb_strtolower($source)
294
+        ) {
295
+            // Forbid changing case only on case-insensitive file system
296
+            return false;
297
+        }
298
+
299
+        $absoluteSource = $this->buildPath($source);
300
+        $absoluteTarget = $this->buildPath($target);
301
+        try {
302
+            $result = $this->share->rename($absoluteSource, $absoluteTarget);
303
+        } catch (AlreadyExistsException $e) {
304
+            if ($retry) {
305
+                $this->remove($target);
306
+                $result = $this->share->rename($absoluteSource, $absoluteTarget);
307
+            } else {
308
+                $this->logger->warning($e->getMessage(), ['exception' => $e]);
309
+                return false;
310
+            }
311
+        } catch (InvalidArgumentException $e) {
312
+            if ($retry) {
313
+                $this->remove($target);
314
+                $result = $this->share->rename($absoluteSource, $absoluteTarget);
315
+            } else {
316
+                $this->logger->warning($e->getMessage(), ['exception' => $e]);
317
+                return false;
318
+            }
319
+        } catch (\Exception $e) {
320
+            $this->logger->warning($e->getMessage(), ['exception' => $e]);
321
+            return false;
322
+        }
323
+        unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
324
+        return $result;
325
+    }
326
+
327
+    public function stat(string $path, bool $retry = true): array|false {
328
+        try {
329
+            $result = $this->formatInfo($this->getFileInfo($path));
330
+        } catch (\OCP\Files\ForbiddenException $e) {
331
+            return false;
332
+        } catch (\OCP\Files\NotFoundException $e) {
333
+            return false;
334
+        } catch (TimedOutException $e) {
335
+            if ($retry) {
336
+                return $this->stat($path, false);
337
+            } else {
338
+                throw $e;
339
+            }
340
+        }
341
+        if ($this->remoteIsShare() && $this->isRootDir($path)) {
342
+            $result['mtime'] = $this->shareMTime();
343
+        }
344
+        return $result;
345
+    }
346
+
347
+    /**
348
+     * get the best guess for the modification time of the share
349
+     */
350
+    private function shareMTime(): int {
351
+        $highestMTime = 0;
352
+        $files = $this->share->dir($this->root);
353
+        foreach ($files as $fileInfo) {
354
+            try {
355
+                if ($fileInfo->getMTime() > $highestMTime) {
356
+                    $highestMTime = $fileInfo->getMTime();
357
+                }
358
+            } catch (NotFoundException $e) {
359
+                // Ignore this, can happen on unavailable DFS shares
360
+            } catch (ForbiddenException $e) {
361
+                // Ignore this too - it's a symlink
362
+            }
363
+        }
364
+        return $highestMTime;
365
+    }
366
+
367
+    /**
368
+     * Check if the path is our root dir (not the smb one)
369
+     */
370
+    private function isRootDir(string $path): bool {
371
+        return $path === '' || $path === '/' || $path === '.';
372
+    }
373
+
374
+    /**
375
+     * Check if our root points to a smb share
376
+     */
377
+    private function remoteIsShare(): bool {
378
+        return $this->share->getName() && (!$this->root || $this->root === '/');
379
+    }
380
+
381
+    public function unlink(string $path): bool {
382
+        if ($this->isRootDir($path)) {
383
+            return false;
384
+        }
385
+
386
+        try {
387
+            if ($this->is_dir($path)) {
388
+                return $this->rmdir($path);
389
+            } else {
390
+                $path = $this->buildPath($path);
391
+                unset($this->statCache[$path]);
392
+                $this->share->del($path);
393
+                return true;
394
+            }
395
+        } catch (NotFoundException $e) {
396
+            return false;
397
+        } catch (ForbiddenException $e) {
398
+            return false;
399
+        } catch (ConnectException $e) {
400
+            $this->logger->error('Error while deleting file', ['exception' => $e]);
401
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
402
+        }
403
+    }
404
+
405
+    /**
406
+     * check if a file or folder has been updated since $time
407
+     */
408
+    public function hasUpdated(string $path, int $time): bool {
409
+        if (!$path and $this->root === '/') {
410
+            // mtime doesn't work for shares, but giving the nature of the backend,
411
+            // doing a full update is still just fast enough
412
+            return true;
413
+        } else {
414
+            $actualTime = $this->filemtime($path);
415
+            return $actualTime > $time || $actualTime === 0;
416
+        }
417
+    }
418
+
419
+    /**
420
+     * @return resource|false
421
+     */
422
+    public function fopen(string $path, string $mode) {
423
+        $fullPath = $this->buildPath($path);
424
+        try {
425
+            switch ($mode) {
426
+                case 'r':
427
+                case 'rb':
428
+                    if (!$this->file_exists($path)) {
429
+                        $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', file doesn\'t exist.');
430
+                        return false;
431
+                    }
432
+                    return $this->share->read($fullPath);
433
+                case 'w':
434
+                case 'wb':
435
+                    $source = $this->share->write($fullPath);
436
+                    return CallBackWrapper::wrap($source, null, null, function () use ($fullPath): void {
437
+                        unset($this->statCache[$fullPath]);
438
+                    });
439
+                case 'a':
440
+                case 'ab':
441
+                case 'r+':
442
+                case 'w+':
443
+                case 'wb+':
444
+                case 'a+':
445
+                case 'x':
446
+                case 'x+':
447
+                case 'c':
448
+                case 'c+':
449
+                    //emulate these
450
+                    if (strrpos($path, '.') !== false) {
451
+                        $ext = substr($path, strrpos($path, '.'));
452
+                    } else {
453
+                        $ext = '';
454
+                    }
455
+                    if ($this->file_exists($path)) {
456
+                        if (!$this->isUpdatable($path)) {
457
+                            $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', file not updatable.');
458
+                            return false;
459
+                        }
460
+                        $tmpFile = $this->getCachedFile($path);
461
+                    } else {
462
+                        if (!$this->isCreatable(dirname($path))) {
463
+                            $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', parent directory not writable.');
464
+                            return false;
465
+                        }
466
+                        $tmpFile = \OCP\Server::get(ITempManager::class)->getTemporaryFile($ext);
467
+                    }
468
+                    $source = fopen($tmpFile, $mode);
469
+                    $share = $this->share;
470
+                    return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share): void {
471
+                        unset($this->statCache[$fullPath]);
472
+                        $share->put($tmpFile, $fullPath);
473
+                        unlink($tmpFile);
474
+                    });
475
+            }
476
+            return false;
477
+        } catch (NotFoundException $e) {
478
+            $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', not found.', ['exception' => $e]);
479
+            return false;
480
+        } catch (ForbiddenException $e) {
481
+            $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', forbidden.', ['exception' => $e]);
482
+            return false;
483
+        } catch (OutOfSpaceException $e) {
484
+            $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', out of space.', ['exception' => $e]);
485
+            throw new EntityTooLargeException('not enough available space to create file', 0, $e);
486
+        } catch (ConnectException $e) {
487
+            $this->logger->error('Error while opening file ' . $path . ' on ' . $this->getId(), ['exception' => $e]);
488
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
489
+        }
490
+    }
491
+
492
+    public function rmdir(string $path): bool {
493
+        if ($this->isRootDir($path)) {
494
+            return false;
495
+        }
496
+
497
+        try {
498
+            $this->statCache = new CappedMemoryCache();
499
+            $content = $this->share->dir($this->buildPath($path));
500
+            foreach ($content as $file) {
501
+                if ($file->isDirectory()) {
502
+                    $this->rmdir($path . '/' . $file->getName());
503
+                } else {
504
+                    $this->share->del($file->getPath());
505
+                }
506
+            }
507
+            $this->share->rmdir($this->buildPath($path));
508
+            return true;
509
+        } catch (NotFoundException $e) {
510
+            return false;
511
+        } catch (ForbiddenException $e) {
512
+            return false;
513
+        } catch (ConnectException $e) {
514
+            $this->logger->error('Error while removing folder', ['exception' => $e]);
515
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
516
+        }
517
+    }
518
+
519
+    public function touch(string $path, ?int $mtime = null): bool {
520
+        try {
521
+            if (!$this->file_exists($path)) {
522
+                $fh = $this->share->write($this->buildPath($path));
523
+                fclose($fh);
524
+                return true;
525
+            }
526
+            return false;
527
+        } catch (OutOfSpaceException $e) {
528
+            throw new EntityTooLargeException('not enough available space to create file', 0, $e);
529
+        } catch (ConnectException $e) {
530
+            $this->logger->error('Error while creating file', ['exception' => $e]);
531
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
532
+        }
533
+    }
534
+
535
+    public function getMetaData(string $path): ?array {
536
+        try {
537
+            $fileInfo = $this->getFileInfo($path);
538
+        } catch (\OCP\Files\NotFoundException $e) {
539
+            return null;
540
+        } catch (\OCP\Files\ForbiddenException $e) {
541
+            return null;
542
+        }
543
+
544
+        return $this->getMetaDataFromFileInfo($fileInfo);
545
+    }
546
+
547
+    private function getMetaDataFromFileInfo(IFileInfo $fileInfo): array {
548
+        $permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
549
+
550
+        if (
551
+            !$fileInfo->isReadOnly() || $fileInfo->isDirectory()
552
+        ) {
553
+            $permissions += Constants::PERMISSION_DELETE;
554
+            $permissions += Constants::PERMISSION_UPDATE;
555
+            if ($fileInfo->isDirectory()) {
556
+                $permissions += Constants::PERMISSION_CREATE;
557
+            }
558
+        }
559
+
560
+        $data = [];
561
+        if ($fileInfo->isDirectory()) {
562
+            $data['mimetype'] = 'httpd/unix-directory';
563
+        } else {
564
+            $data['mimetype'] = \OCP\Server::get(IMimeTypeDetector::class)->detectPath($fileInfo->getPath());
565
+        }
566
+        $data['mtime'] = $fileInfo->getMTime();
567
+        if ($fileInfo->isDirectory()) {
568
+            $data['size'] = -1; //unknown
569
+        } else {
570
+            $data['size'] = $fileInfo->getSize();
571
+        }
572
+        $data['etag'] = $this->getETag($fileInfo->getPath());
573
+        $data['storage_mtime'] = $data['mtime'];
574
+        $data['permissions'] = $permissions;
575
+        $data['name'] = $fileInfo->getName();
576
+
577
+        return $data;
578
+    }
579
+
580
+    public function opendir(string $path) {
581
+        try {
582
+            $files = $this->getFolderContents($path);
583
+        } catch (NotFoundException $e) {
584
+            return false;
585
+        } catch (NotPermittedException $e) {
586
+            return false;
587
+        }
588
+        $names = array_map(function ($info) {
589
+            /** @var IFileInfo $info */
590
+            return $info->getName();
591
+        }, iterator_to_array($files));
592
+        return IteratorDirectory::wrap($names);
593
+    }
594
+
595
+    public function getDirectoryContent(string $directory): \Traversable {
596
+        try {
597
+            $files = $this->getFolderContents($directory);
598
+            foreach ($files as $file) {
599
+                yield $this->getMetaDataFromFileInfo($file);
600
+            }
601
+        } catch (NotFoundException $e) {
602
+            return;
603
+        } catch (NotPermittedException $e) {
604
+            return;
605
+        }
606
+    }
607
+
608
+    public function filetype(string $path): string|false {
609
+        try {
610
+            return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
611
+        } catch (\OCP\Files\NotFoundException $e) {
612
+            return false;
613
+        } catch (\OCP\Files\ForbiddenException $e) {
614
+            return false;
615
+        }
616
+    }
617
+
618
+    public function mkdir(string $path): bool {
619
+        $path = $this->buildPath($path);
620
+        try {
621
+            $this->share->mkdir($path);
622
+            return true;
623
+        } catch (ConnectException $e) {
624
+            $this->logger->error('Error while creating folder', ['exception' => $e]);
625
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
626
+        } catch (Exception $e) {
627
+            return false;
628
+        }
629
+    }
630
+
631
+    public function file_exists(string $path): bool {
632
+        try {
633
+            // Case sensitive filesystem doesn't matter for root directory
634
+            if ($this->caseSensitive === false && $path !== '') {
635
+                $filename = basename($path);
636
+                $siblings = $this->getDirectoryContent(dirname($path));
637
+                foreach ($siblings as $sibling) {
638
+                    if ($sibling['name'] === $filename) {
639
+                        return true;
640
+                    }
641
+                }
642
+                return false;
643
+            }
644
+            $this->getFileInfo($path);
645
+            return true;
646
+        } catch (\OCP\Files\NotFoundException $e) {
647
+            return false;
648
+        } catch (\OCP\Files\ForbiddenException $e) {
649
+            return false;
650
+        } catch (ConnectException $e) {
651
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
652
+        }
653
+    }
654
+
655
+    public function isReadable(string $path): bool {
656
+        try {
657
+            $info = $this->getFileInfo($path);
658
+            return $this->showHidden || !$info->isHidden();
659
+        } catch (\OCP\Files\NotFoundException $e) {
660
+            return false;
661
+        } catch (\OCP\Files\ForbiddenException $e) {
662
+            return false;
663
+        }
664
+    }
665
+
666
+    public function isUpdatable(string $path): bool {
667
+        try {
668
+            $info = $this->getFileInfo($path);
669
+            // following windows behaviour for read-only folders: they can be written into
670
+            // (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
671
+            return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory());
672
+        } catch (\OCP\Files\NotFoundException $e) {
673
+            return false;
674
+        } catch (\OCP\Files\ForbiddenException $e) {
675
+            return false;
676
+        }
677
+    }
678
+
679
+    public function isDeletable(string $path): bool {
680
+        try {
681
+            $info = $this->getFileInfo($path);
682
+            return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
683
+        } catch (\OCP\Files\NotFoundException $e) {
684
+            return false;
685
+        } catch (\OCP\Files\ForbiddenException $e) {
686
+            return false;
687
+        }
688
+    }
689
+
690
+    /**
691
+     * check if smbclient is installed
692
+     */
693
+    public static function checkDependencies(): array|bool {
694
+        $system = \OCP\Server::get(SystemBridge::class);
695
+        return Server::available($system) || NativeServer::available($system) ?: ['smbclient'];
696
+    }
697
+
698
+    public function test(): bool {
699
+        try {
700
+            return parent::test();
701
+        } catch (StorageAuthException $e) {
702
+            return false;
703
+        } catch (ForbiddenException $e) {
704
+            return false;
705
+        } catch (Exception $e) {
706
+            $this->logger->error($e->getMessage(), ['exception' => $e]);
707
+            return false;
708
+        }
709
+    }
710
+
711
+    public function listen(string $path, callable $callback): void {
712
+        $this->notify($path)->listen(function (IChange $change) use ($callback) {
713
+            if ($change instanceof IRenameChange) {
714
+                return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
715
+            } else {
716
+                return $callback($change->getType(), $change->getPath());
717
+            }
718
+        });
719
+    }
720
+
721
+    public function notify(string $path): SMBNotifyHandler {
722
+        $path = '/' . ltrim($path, '/');
723
+        $shareNotifyHandler = $this->share->notify($this->buildPath($path));
724
+        return new SMBNotifyHandler($shareNotifyHandler, $this->root);
725
+    }
726 726
 }
Please login to merge, or discard this patch.
Spacing   +34 added lines, -34 removed lines patch added patch discarded remove patch
@@ -90,7 +90,7 @@  discard block
 block discarded – undo
90 90
 				throw new \Exception(
91 91
 					'Invalid logger. Got '
92 92
 					. get_class($parameters['logger'])
93
-					. ' Expected ' . LoggerInterface::class
93
+					. ' Expected '.LoggerInterface::class
94 94
 				);
95 95
 			}
96 96
 			$this->logger = $parameters['logger'];
@@ -100,7 +100,7 @@  discard block
 block discarded – undo
100 100
 
101 101
 		$options = new Options();
102 102
 		if (isset($parameters['timeout'])) {
103
-			$timeout = (int)$parameters['timeout'];
103
+			$timeout = (int) $parameters['timeout'];
104 104
 			if ($timeout > 0) {
105 105
 				$options->setTimeout($timeout);
106 106
 			}
@@ -111,11 +111,11 @@  discard block
 block discarded – undo
111 111
 		$this->share = $this->server->getShare(trim($parameters['share'], '/'));
112 112
 
113 113
 		$this->root = $parameters['root'] ?? '/';
114
-		$this->root = '/' . ltrim($this->root, '/');
115
-		$this->root = rtrim($this->root, '/') . '/';
114
+		$this->root = '/'.ltrim($this->root, '/');
115
+		$this->root = rtrim($this->root, '/').'/';
116 116
 
117 117
 		$this->showHidden = isset($parameters['show_hidden']) && $parameters['show_hidden'];
118
-		$this->caseSensitive = (bool)($parameters['case_sensitive'] ?? true);
118
+		$this->caseSensitive = (bool) ($parameters['case_sensitive'] ?? true);
119 119
 		$this->checkAcl = isset($parameters['check_acl']) && $parameters['check_acl'];
120 120
 
121 121
 		$this->statCache = new CappedMemoryCache();
@@ -136,11 +136,11 @@  discard block
 block discarded – undo
136 136
 		// FIXME: double slash to keep compatible with the old storage ids,
137 137
 		// failure to do so will lead to creation of a new storage id and
138 138
 		// loss of shares from the storage
139
-		return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
139
+		return 'smb::'.$this->server->getAuth()->getUsername().'@'.$this->server->getHost().'//'.$this->share->getName().'/'.$this->root;
140 140
 	}
141 141
 
142 142
 	protected function buildPath(string $path): string {
143
-		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
143
+		return Filesystem::normalizePath($this->root.'/'.$path, true, false, true);
144 144
 	}
145 145
 
146 146
 	protected function relativePath(string $fullPath): ?string {
@@ -227,7 +227,7 @@  discard block
 block discarded – undo
227 227
 				return;
228 228
 			}
229 229
 			foreach ($files as $file) {
230
-				$this->statCache[$path . '/' . $file->getName()] = $file;
230
+				$this->statCache[$path.'/'.$file->getName()] = $file;
231 231
 			}
232 232
 
233 233
 			foreach ($files as $file) {
@@ -241,13 +241,13 @@  discard block
 block discarded – undo
241 241
 						// this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
242 242
 						// additionally, it's better to have false negatives here then false positives
243 243
 						if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
244
-							$this->logger->debug('Hiding non readable entry ' . $file->getName());
244
+							$this->logger->debug('Hiding non readable entry '.$file->getName());
245 245
 							continue;
246 246
 						}
247 247
 					}
248 248
 
249 249
 					if ($hide) {
250
-						$this->logger->debug('hiding hidden file ' . $file->getName());
250
+						$this->logger->debug('hiding hidden file '.$file->getName());
251 251
 					}
252 252
 					if (!$hide) {
253 253
 						yield $file;
@@ -255,12 +255,12 @@  discard block
 block discarded – undo
255 255
 				} catch (ForbiddenException $e) {
256 256
 					$this->logger->debug($e->getMessage(), ['exception' => $e]);
257 257
 				} catch (NotFoundException $e) {
258
-					$this->logger->debug('Hiding forbidden entry ' . $file->getName(), ['exception' => $e]);
258
+					$this->logger->debug('Hiding forbidden entry '.$file->getName(), ['exception' => $e]);
259 259
 				}
260 260
 			}
261 261
 		} catch (ConnectException $e) {
262 262
 			$this->logger->error('Error while getting folder content', ['exception' => $e]);
263
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
263
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
264 264
 		} catch (NotFoundException $e) {
265 265
 			throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
266 266
 		}
@@ -324,7 +324,7 @@  discard block
 block discarded – undo
324 324
 		return $result;
325 325
 	}
326 326
 
327
-	public function stat(string $path, bool $retry = true): array|false {
327
+	public function stat(string $path, bool $retry = true): array | false {
328 328
 		try {
329 329
 			$result = $this->formatInfo($this->getFileInfo($path));
330 330
 		} catch (\OCP\Files\ForbiddenException $e) {
@@ -398,7 +398,7 @@  discard block
 block discarded – undo
398 398
 			return false;
399 399
 		} catch (ConnectException $e) {
400 400
 			$this->logger->error('Error while deleting file', ['exception' => $e]);
401
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
401
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
402 402
 		}
403 403
 	}
404 404
 
@@ -426,14 +426,14 @@  discard block
 block discarded – undo
426 426
 				case 'r':
427 427
 				case 'rb':
428 428
 					if (!$this->file_exists($path)) {
429
-						$this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', file doesn\'t exist.');
429
+						$this->logger->warning('Failed to open '.$path.' on '.$this->getId().', file doesn\'t exist.');
430 430
 						return false;
431 431
 					}
432 432
 					return $this->share->read($fullPath);
433 433
 				case 'w':
434 434
 				case 'wb':
435 435
 					$source = $this->share->write($fullPath);
436
-					return CallBackWrapper::wrap($source, null, null, function () use ($fullPath): void {
436
+					return CallBackWrapper::wrap($source, null, null, function() use ($fullPath): void {
437 437
 						unset($this->statCache[$fullPath]);
438 438
 					});
439 439
 				case 'a':
@@ -454,20 +454,20 @@  discard block
 block discarded – undo
454 454
 					}
455 455
 					if ($this->file_exists($path)) {
456 456
 						if (!$this->isUpdatable($path)) {
457
-							$this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', file not updatable.');
457
+							$this->logger->warning('Failed to open '.$path.' on '.$this->getId().', file not updatable.');
458 458
 							return false;
459 459
 						}
460 460
 						$tmpFile = $this->getCachedFile($path);
461 461
 					} else {
462 462
 						if (!$this->isCreatable(dirname($path))) {
463
-							$this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', parent directory not writable.');
463
+							$this->logger->warning('Failed to open '.$path.' on '.$this->getId().', parent directory not writable.');
464 464
 							return false;
465 465
 						}
466 466
 						$tmpFile = \OCP\Server::get(ITempManager::class)->getTemporaryFile($ext);
467 467
 					}
468 468
 					$source = fopen($tmpFile, $mode);
469 469
 					$share = $this->share;
470
-					return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share): void {
470
+					return CallbackWrapper::wrap($source, null, null, function() use ($tmpFile, $fullPath, $share): void {
471 471
 						unset($this->statCache[$fullPath]);
472 472
 						$share->put($tmpFile, $fullPath);
473 473
 						unlink($tmpFile);
@@ -475,17 +475,17 @@  discard block
 block discarded – undo
475 475
 			}
476 476
 			return false;
477 477
 		} catch (NotFoundException $e) {
478
-			$this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', not found.', ['exception' => $e]);
478
+			$this->logger->warning('Failed to open '.$path.' on '.$this->getId().', not found.', ['exception' => $e]);
479 479
 			return false;
480 480
 		} catch (ForbiddenException $e) {
481
-			$this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', forbidden.', ['exception' => $e]);
481
+			$this->logger->warning('Failed to open '.$path.' on '.$this->getId().', forbidden.', ['exception' => $e]);
482 482
 			return false;
483 483
 		} catch (OutOfSpaceException $e) {
484
-			$this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', out of space.', ['exception' => $e]);
484
+			$this->logger->warning('Failed to open '.$path.' on '.$this->getId().', out of space.', ['exception' => $e]);
485 485
 			throw new EntityTooLargeException('not enough available space to create file', 0, $e);
486 486
 		} catch (ConnectException $e) {
487
-			$this->logger->error('Error while opening file ' . $path . ' on ' . $this->getId(), ['exception' => $e]);
488
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
487
+			$this->logger->error('Error while opening file '.$path.' on '.$this->getId(), ['exception' => $e]);
488
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
489 489
 		}
490 490
 	}
491 491
 
@@ -499,7 +499,7 @@  discard block
 block discarded – undo
499 499
 			$content = $this->share->dir($this->buildPath($path));
500 500
 			foreach ($content as $file) {
501 501
 				if ($file->isDirectory()) {
502
-					$this->rmdir($path . '/' . $file->getName());
502
+					$this->rmdir($path.'/'.$file->getName());
503 503
 				} else {
504 504
 					$this->share->del($file->getPath());
505 505
 				}
@@ -512,7 +512,7 @@  discard block
 block discarded – undo
512 512
 			return false;
513 513
 		} catch (ConnectException $e) {
514 514
 			$this->logger->error('Error while removing folder', ['exception' => $e]);
515
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
515
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
516 516
 		}
517 517
 	}
518 518
 
@@ -528,7 +528,7 @@  discard block
 block discarded – undo
528 528
 			throw new EntityTooLargeException('not enough available space to create file', 0, $e);
529 529
 		} catch (ConnectException $e) {
530 530
 			$this->logger->error('Error while creating file', ['exception' => $e]);
531
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
531
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
532 532
 		}
533 533
 	}
534 534
 
@@ -585,7 +585,7 @@  discard block
 block discarded – undo
585 585
 		} catch (NotPermittedException $e) {
586 586
 			return false;
587 587
 		}
588
-		$names = array_map(function ($info) {
588
+		$names = array_map(function($info) {
589 589
 			/** @var IFileInfo $info */
590 590
 			return $info->getName();
591 591
 		}, iterator_to_array($files));
@@ -605,7 +605,7 @@  discard block
 block discarded – undo
605 605
 		}
606 606
 	}
607 607
 
608
-	public function filetype(string $path): string|false {
608
+	public function filetype(string $path): string | false {
609 609
 		try {
610 610
 			return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
611 611
 		} catch (\OCP\Files\NotFoundException $e) {
@@ -622,7 +622,7 @@  discard block
 block discarded – undo
622 622
 			return true;
623 623
 		} catch (ConnectException $e) {
624 624
 			$this->logger->error('Error while creating folder', ['exception' => $e]);
625
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
625
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
626 626
 		} catch (Exception $e) {
627 627
 			return false;
628 628
 		}
@@ -648,7 +648,7 @@  discard block
 block discarded – undo
648 648
 		} catch (\OCP\Files\ForbiddenException $e) {
649 649
 			return false;
650 650
 		} catch (ConnectException $e) {
651
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
651
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
652 652
 		}
653 653
 	}
654 654
 
@@ -690,7 +690,7 @@  discard block
 block discarded – undo
690 690
 	/**
691 691
 	 * check if smbclient is installed
692 692
 	 */
693
-	public static function checkDependencies(): array|bool {
693
+	public static function checkDependencies(): array | bool {
694 694
 		$system = \OCP\Server::get(SystemBridge::class);
695 695
 		return Server::available($system) || NativeServer::available($system) ?: ['smbclient'];
696 696
 	}
@@ -709,7 +709,7 @@  discard block
 block discarded – undo
709 709
 	}
710 710
 
711 711
 	public function listen(string $path, callable $callback): void {
712
-		$this->notify($path)->listen(function (IChange $change) use ($callback) {
712
+		$this->notify($path)->listen(function(IChange $change) use ($callback) {
713 713
 			if ($change instanceof IRenameChange) {
714 714
 				return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
715 715
 			} else {
@@ -719,7 +719,7 @@  discard block
 block discarded – undo
719 719
 	}
720 720
 
721 721
 	public function notify(string $path): SMBNotifyHandler {
722
-		$path = '/' . ltrim($path, '/');
722
+		$path = '/'.ltrim($path, '/');
723 723
 		$shareNotifyHandler = $this->share->notify($this->buildPath($path));
724 724
 		return new SMBNotifyHandler($shareNotifyHandler, $this->root);
725 725
 	}
Please login to merge, or discard this patch.