SMB::getFolderContents()   F
last analyzed

Complexity

Conditions 16
Paths 477

Size

Total Lines 48
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
eloc 31
c 0
b 0
f 0
nc 477
nop 1
dl 0
loc 48
rs 2.1263

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Christoph Wurst <[email protected]>
7
 * @author Jesús Macias <[email protected]>
8
 * @author Jörn Friedrich Dreyer <[email protected]>
9
 * @author Juan Pablo Villafañez <[email protected]>
10
 * @author Juan Pablo Villafáñez <[email protected]>
11
 * @author Julius Härtl <[email protected]>
12
 * @author Michael Gapczynski <[email protected]>
13
 * @author Morris Jobke <[email protected]>
14
 * @author Philipp Kapfer <[email protected]>
15
 * @author Robin Appelman <[email protected]>
16
 * @author Robin McCorkell <[email protected]>
17
 * @author Roeland Jago Douma <[email protected]>
18
 * @author Roland Tapken <[email protected]>
19
 * @author Thomas Müller <[email protected]>
20
 * @author Vincent Petry <[email protected]>
21
 *
22
 * @license AGPL-3.0
23
 *
24
 * This code is free software: you can redistribute it and/or modify
25
 * it under the terms of the GNU Affero General Public License, version 3,
26
 * as published by the Free Software Foundation.
27
 *
28
 * This program is distributed in the hope that it will be useful,
29
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
30
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31
 * GNU Affero General Public License for more details.
32
 *
33
 * You should have received a copy of the GNU Affero General Public License, version 3,
34
 * along with this program. If not, see <http://www.gnu.org/licenses/>
35
 *
36
 */
37
namespace OCA\Files_External\Lib\Storage;
38
39
use Icewind\SMB\ACL;
40
use Icewind\SMB\BasicAuth;
41
use Icewind\SMB\Exception\AlreadyExistsException;
42
use Icewind\SMB\Exception\ConnectException;
43
use Icewind\SMB\Exception\Exception;
44
use Icewind\SMB\Exception\ForbiddenException;
45
use Icewind\SMB\Exception\InvalidArgumentException;
46
use Icewind\SMB\Exception\InvalidTypeException;
47
use Icewind\SMB\Exception\NotFoundException;
48
use Icewind\SMB\Exception\OutOfSpaceException;
49
use Icewind\SMB\Exception\TimedOutException;
50
use Icewind\SMB\IFileInfo;
51
use Icewind\SMB\Native\NativeServer;
52
use Icewind\SMB\Options;
53
use Icewind\SMB\ServerFactory;
54
use Icewind\SMB\System;
55
use Icewind\Streams\CallbackWrapper;
56
use Icewind\Streams\IteratorDirectory;
57
use OCP\Cache\CappedMemoryCache;
58
use OC\Files\Filesystem;
59
use OC\Files\Storage\Common;
60
use OCA\Files_External\Lib\Notify\SMBNotifyHandler;
61
use OCP\Constants;
62
use OCP\Files\EntityTooLargeException;
63
use OCP\Files\Notify\IChange;
64
use OCP\Files\Notify\IRenameChange;
65
use OCP\Files\NotPermittedException;
66
use OCP\Files\Storage\INotifyStorage;
67
use OCP\Files\StorageAuthException;
68
use OCP\Files\StorageNotAvailableException;
69
use OCP\ILogger;
70
71
class SMB extends Common implements INotifyStorage {
72
	/**
73
	 * @var \Icewind\SMB\IServer
74
	 */
75
	protected $server;
76
77
	/**
78
	 * @var \Icewind\SMB\IShare
79
	 */
80
	protected $share;
81
82
	/**
83
	 * @var string
84
	 */
85
	protected $root;
86
87
	/** @var CappedMemoryCache<IFileInfo> */
88
	protected CappedMemoryCache $statCache;
89
90
	/** @var ILogger */
91
	protected $logger;
92
93
	/** @var bool */
94
	protected $showHidden;
95
96
	/** @var bool */
97
	protected $checkAcl;
98
99
	public function __construct($params) {
100
		if (!isset($params['host'])) {
101
			throw new \Exception('Invalid configuration, no host provided');
102
		}
103
104
		if (isset($params['auth'])) {
105
			$auth = $params['auth'];
106
		} elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
107
			[$workgroup, $user] = $this->splitUser($params['user']);
108
			$auth = new BasicAuth($user, $workgroup, $params['password']);
109
		} else {
110
			throw new \Exception('Invalid configuration, no credentials provided');
111
		}
112
113
		if (isset($params['logger'])) {
114
			$this->logger = $params['logger'];
115
		} else {
116
			$this->logger = \OC::$server->getLogger();
117
		}
118
119
		$options = new Options();
120
		if (isset($params['timeout'])) {
121
			$timeout = (int)$params['timeout'];
122
			if ($timeout > 0) {
123
				$options->setTimeout($timeout);
124
			}
125
		}
126
		$serverFactory = new ServerFactory($options);
127
		$this->server = $serverFactory->createServer($params['host'], $auth);
128
		$this->share = $this->server->getShare(trim($params['share'], '/'));
129
130
		$this->root = $params['root'] ?? '/';
131
		$this->root = '/' . ltrim($this->root, '/');
132
		$this->root = rtrim($this->root, '/') . '/';
133
134
		$this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
135
		$this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
136
137
		$this->statCache = new CappedMemoryCache();
138
		parent::__construct($params);
139
	}
140
141
	private function splitUser($user) {
142
		if (str_contains($user, '/')) {
143
			return explode('/', $user, 2);
144
		} elseif (str_contains($user, '\\')) {
145
			return explode('\\', $user);
146
		}
147
148
		return [null, $user];
149
	}
150
151
	/**
152
	 * @return string
153
	 */
154
	public function getId() {
155
		// FIXME: double slash to keep compatible with the old storage ids,
156
		// failure to do so will lead to creation of a new storage id and
157
		// loss of shares from the storage
158
		return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
159
	}
160
161
	/**
162
	 * @param string $path
163
	 * @return string
164
	 */
165
	protected function buildPath($path) {
166
		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
167
	}
168
169
	protected function relativePath($fullPath) {
170
		if ($fullPath === $this->root) {
171
			return '';
172
		} elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
173
			return substr($fullPath, strlen($this->root));
174
		} else {
175
			return null;
176
		}
177
	}
178
179
	/**
180
	 * @param string $path
181
	 * @return IFileInfo
182
	 * @throws StorageAuthException
183
	 */
184
	protected function getFileInfo($path) {
185
		try {
186
			$path = $this->buildPath($path);
187
			$cached = $this->statCache[$path] ?? null;
188
			if ($cached instanceof IFileInfo) {
189
				return $cached;
190
			} else {
191
				$stat = $this->share->stat($path);
192
				$this->statCache[$path] = $stat;
193
				return $stat;
194
			}
195
		} catch (ConnectException $e) {
196
			$this->throwUnavailable($e);
197
		} catch (NotFoundException $e) {
198
			throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
199
		} catch (ForbiddenException $e) {
200
			// with php-smbclient, this exception is thrown when the provided password is invalid.
201
			// Possible is also ForbiddenException with a different error code, so we check it.
202
			if ($e->getCode() === 1) {
203
				$this->throwUnavailable($e);
204
			}
205
			throw new \OCP\Files\ForbiddenException($e->getMessage(), false, $e);
206
		}
207
	}
208
209
	/**
210
	 * @param \Exception $e
211
	 * @return never
212
	 * @throws StorageAuthException
213
	 */
214
	protected function throwUnavailable(\Exception $e) {
215
		$this->logger->logException($e, ['message' => 'Error while getting file info']);
216
		throw new StorageAuthException($e->getMessage(), $e);
217
	}
218
219
	/**
220
	 * get the acl from fileinfo that is relevant for the configured user
221
	 *
222
	 * @param IFileInfo $file
223
	 * @return ACL|null
224
	 */
225
	private function getACL(IFileInfo $file): ?ACL {
226
		$acls = $file->getAcls();
227
		foreach ($acls as $user => $acl) {
228
			[, $user] = $this->splitUser($user); // strip domain
229
			if ($user === $this->server->getAuth()->getUsername()) {
230
				return $acl;
231
			}
232
		}
233
234
		return null;
235
	}
236
237
	/**
238
	 * @param string $path
239
	 * @return \Generator<IFileInfo>
240
	 * @throws StorageNotAvailableException
241
	 */
242
	protected function getFolderContents($path): iterable {
243
		try {
244
			$path = ltrim($this->buildPath($path), '/');
245
			try {
246
				$files = $this->share->dir($path);
247
			} catch (ForbiddenException $e) {
248
				$this->logger->critical($e->getMessage(), ['exception' => $e]);
249
				throw new NotPermittedException();
250
			} catch (InvalidTypeException $e) {
251
				return;
252
			}
253
			foreach ($files as $file) {
254
				$this->statCache[$path . '/' . $file->getName()] = $file;
255
			}
256
257
			foreach ($files as $file) {
258
				try {
259
					// the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
260
					// so we trigger the below exceptions where applicable
261
					$hide = $file->isHidden() && !$this->showHidden;
262
263
					if ($this->checkAcl && $acl = $this->getACL($file)) {
264
						// if there is no explicit deny, we assume it's allowed
265
						// this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
266
						// additionally, it's better to have false negatives here then false positives
267
						if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
268
							$this->logger->debug('Hiding non readable entry ' . $file->getName());
269
							continue;
270
						}
271
					}
272
273
					if ($hide) {
274
						$this->logger->debug('hiding hidden file ' . $file->getName());
275
					}
276
					if (!$hide) {
277
						yield $file;
278
					}
279
				} catch (ForbiddenException $e) {
280
					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::DEBUG has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

280
					$this->logger->logException($e, ['level' => /** @scrutinizer ignore-deprecated */ ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
281
				} catch (NotFoundException $e) {
282
					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::DEBUG has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

282
					$this->logger->logException($e, ['level' => /** @scrutinizer ignore-deprecated */ ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
283
				}
284
			}
285
		} catch (ConnectException $e) {
286
			$this->logger->logException($e, ['message' => 'Error while getting folder content']);
287
			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
288
		} catch (NotFoundException $e) {
289
			throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
290
		}
291
	}
292
293
	/**
294
	 * @param IFileInfo $info
295
	 * @return array
296
	 */
297
	protected function formatInfo($info) {
298
		$result = [
299
			'size' => $info->getSize(),
300
			'mtime' => $info->getMTime(),
301
		];
302
		if ($info->isDirectory()) {
303
			$result['type'] = 'dir';
304
		} else {
305
			$result['type'] = 'file';
306
		}
307
		return $result;
308
	}
309
310
	/**
311
	 * Rename the files. If the source or the target is the root, the rename won't happen.
312
	 *
313
	 * @param string $source the old name of the path
314
	 * @param string $target the new name of the path
315
	 * @return bool true if the rename is successful, false otherwise
316
	 */
317
	public function rename($source, $target, $retry = true): bool {
318
		if ($this->isRootDir($source) || $this->isRootDir($target)) {
319
			return false;
320
		}
321
322
		$absoluteSource = $this->buildPath($source);
323
		$absoluteTarget = $this->buildPath($target);
324
		try {
325
			$result = $this->share->rename($absoluteSource, $absoluteTarget);
326
		} catch (AlreadyExistsException $e) {
327
			if ($retry) {
328
				$this->remove($target);
329
				$result = $this->share->rename($absoluteSource, $absoluteTarget);
330
			} else {
331
				$this->logger->logException($e, ['level' => ILogger::WARN]);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::WARN has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

331
				$this->logger->logException($e, ['level' => /** @scrutinizer ignore-deprecated */ ILogger::WARN]);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
332
				return false;
333
			}
334
		} catch (InvalidArgumentException $e) {
335
			if ($retry) {
336
				$this->remove($target);
337
				$result = $this->share->rename($absoluteSource, $absoluteTarget);
338
			} else {
339
				$this->logger->logException($e, ['level' => ILogger::WARN]);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::WARN has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

339
				$this->logger->logException($e, ['level' => /** @scrutinizer ignore-deprecated */ ILogger::WARN]);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
340
				return false;
341
			}
342
		} catch (\Exception $e) {
343
			$this->logger->logException($e, ['level' => ILogger::WARN]);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::WARN has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

343
			$this->logger->logException($e, ['level' => /** @scrutinizer ignore-deprecated */ ILogger::WARN]);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
344
			return false;
345
		}
346
		unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
347
		return $result;
348
	}
349
350
	public function stat($path, $retry = true) {
351
		try {
352
			$result = $this->formatInfo($this->getFileInfo($path));
353
		} catch (ForbiddenException $e) {
354
			return false;
355
		} catch (\OCP\Files\NotFoundException $e) {
356
			return false;
357
		} catch (TimedOutException $e) {
358
			if ($retry) {
359
				return $this->stat($path, false);
360
			} else {
361
				throw $e;
362
			}
363
		}
364
		if ($this->remoteIsShare() && $this->isRootDir($path)) {
365
			$result['mtime'] = $this->shareMTime();
366
		}
367
		return $result;
368
	}
369
370
	/**
371
	 * get the best guess for the modification time of the share
372
	 *
373
	 * @return int
374
	 */
375
	private function shareMTime() {
376
		$highestMTime = 0;
377
		$files = $this->share->dir($this->root);
378
		foreach ($files as $fileInfo) {
379
			try {
380
				if ($fileInfo->getMTime() > $highestMTime) {
381
					$highestMTime = $fileInfo->getMTime();
382
				}
383
			} catch (NotFoundException $e) {
384
				// Ignore this, can happen on unavailable DFS shares
385
			} catch (ForbiddenException $e) {
386
				// Ignore this too - it's a symlink
387
			}
388
		}
389
		return $highestMTime;
390
	}
391
392
	/**
393
	 * Check if the path is our root dir (not the smb one)
394
	 *
395
	 * @param string $path the path
396
	 * @return bool
397
	 */
398
	private function isRootDir($path) {
399
		return $path === '' || $path === '/' || $path === '.';
400
	}
401
402
	/**
403
	 * Check if our root points to a smb share
404
	 *
405
	 * @return bool true if our root points to a share false otherwise
406
	 */
407
	private function remoteIsShare() {
408
		return $this->share->getName() && (!$this->root || $this->root === '/');
409
	}
410
411
	/**
412
	 * @param string $path
413
	 * @return bool
414
	 */
415
	public function unlink($path) {
416
		if ($this->isRootDir($path)) {
417
			return false;
418
		}
419
420
		try {
421
			if ($this->is_dir($path)) {
422
				return $this->rmdir($path);
423
			} else {
424
				$path = $this->buildPath($path);
425
				unset($this->statCache[$path]);
426
				$this->share->del($path);
427
				return true;
428
			}
429
		} catch (NotFoundException $e) {
430
			return false;
431
		} catch (ForbiddenException $e) {
432
			return false;
433
		} catch (ConnectException $e) {
434
			$this->logger->logException($e, ['message' => 'Error while deleting file']);
435
			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
436
		}
437
	}
438
439
	/**
440
	 * check if a file or folder has been updated since $time
441
	 *
442
	 * @param string $path
443
	 * @param int $time
444
	 * @return bool
445
	 */
446
	public function hasUpdated($path, $time) {
447
		if (!$path and $this->root === '/') {
448
			// mtime doesn't work for shares, but giving the nature of the backend,
449
			// doing a full update is still just fast enough
450
			return true;
451
		} else {
452
			$actualTime = $this->filemtime($path);
453
			return $actualTime > $time;
454
		}
455
	}
456
457
	/**
458
	 * @param string $path
459
	 * @param string $mode
460
	 * @return resource|bool
461
	 */
462
	public function fopen($path, $mode) {
463
		$fullPath = $this->buildPath($path);
464
		try {
465
			switch ($mode) {
466
				case 'r':
467
				case 'rb':
468
					if (!$this->file_exists($path)) {
469
						return false;
470
					}
471
					return $this->share->read($fullPath);
472
				case 'w':
473
				case 'wb':
474
					$source = $this->share->write($fullPath);
475
					return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
476
						unset($this->statCache[$fullPath]);
477
					});
478
				case 'a':
479
				case 'ab':
480
				case 'r+':
481
				case 'w+':
482
				case 'wb+':
483
				case 'a+':
484
				case 'x':
485
				case 'x+':
486
				case 'c':
487
				case 'c+':
488
					//emulate these
489
					if (strrpos($path, '.') !== false) {
490
						$ext = substr($path, strrpos($path, '.'));
491
					} else {
492
						$ext = '';
493
					}
494
					if ($this->file_exists($path)) {
495
						if (!$this->isUpdatable($path)) {
496
							return false;
497
						}
498
						$tmpFile = $this->getCachedFile($path);
499
					} else {
500
						if (!$this->isCreatable(dirname($path))) {
501
							return false;
502
						}
503
						$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
504
					}
505
					$source = fopen($tmpFile, $mode);
506
					$share = $this->share;
507
					return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
508
						unset($this->statCache[$fullPath]);
509
						$share->put($tmpFile, $fullPath);
510
						unlink($tmpFile);
511
					});
512
			}
513
			return false;
514
		} catch (NotFoundException $e) {
515
			return false;
516
		} catch (ForbiddenException $e) {
517
			return false;
518
		} catch (OutOfSpaceException $e) {
519
			throw new EntityTooLargeException("not enough available space to create file", 0, $e);
520
		} catch (ConnectException $e) {
521
			$this->logger->logException($e, ['message' => 'Error while opening file']);
522
			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
523
		}
524
	}
525
526
	public function rmdir($path) {
527
		if ($this->isRootDir($path)) {
528
			return false;
529
		}
530
531
		try {
532
			$this->statCache = new CappedMemoryCache();
533
			$content = $this->share->dir($this->buildPath($path));
534
			foreach ($content as $file) {
535
				if ($file->isDirectory()) {
536
					$this->rmdir($path . '/' . $file->getName());
537
				} else {
538
					$this->share->del($file->getPath());
539
				}
540
			}
541
			$this->share->rmdir($this->buildPath($path));
542
			return true;
543
		} catch (NotFoundException $e) {
544
			return false;
545
		} catch (ForbiddenException $e) {
546
			return false;
547
		} catch (ConnectException $e) {
548
			$this->logger->logException($e, ['message' => 'Error while removing folder']);
549
			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
550
		}
551
	}
552
553
	public function touch($path, $mtime = null) {
554
		try {
555
			if (!$this->file_exists($path)) {
556
				$fh = $this->share->write($this->buildPath($path));
557
				fclose($fh);
558
				return true;
559
			}
560
			return false;
561
		} catch (OutOfSpaceException $e) {
562
			throw new EntityTooLargeException("not enough available space to create file", 0, $e);
563
		} catch (ConnectException $e) {
564
			$this->logger->logException($e, ['message' => 'Error while creating file']);
565
			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
566
		}
567
	}
568
569
	public function getMetaData($path) {
570
		try {
571
			$fileInfo = $this->getFileInfo($path);
572
		} catch (\OCP\Files\NotFoundException $e) {
573
			return null;
574
		} catch (ForbiddenException $e) {
575
			return null;
576
		}
577
		if (!$fileInfo) {
0 ignored issues
show
introduced by
$fileInfo is of type Icewind\SMB\IFileInfo, thus it always evaluated to true.
Loading history...
578
			return null;
579
		}
580
581
		return $this->getMetaDataFromFileInfo($fileInfo);
582
	}
583
584
	private function getMetaDataFromFileInfo(IFileInfo $fileInfo) {
585
		$permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
586
587
		if (
588
			!$fileInfo->isReadOnly() || $fileInfo->isDirectory()
589
		) {
590
			$permissions += Constants::PERMISSION_DELETE;
591
			$permissions += Constants::PERMISSION_UPDATE;
592
			if ($fileInfo->isDirectory()) {
593
				$permissions += Constants::PERMISSION_CREATE;
594
			}
595
		}
596
597
		$data = [];
598
		if ($fileInfo->isDirectory()) {
599
			$data['mimetype'] = 'httpd/unix-directory';
600
		} else {
601
			$data['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath());
602
		}
603
		$data['mtime'] = $fileInfo->getMTime();
604
		if ($fileInfo->isDirectory()) {
605
			$data['size'] = -1; //unknown
606
		} else {
607
			$data['size'] = $fileInfo->getSize();
608
		}
609
		$data['etag'] = $this->getETag($fileInfo->getPath());
610
		$data['storage_mtime'] = $data['mtime'];
611
		$data['permissions'] = $permissions;
612
		$data['name'] = $fileInfo->getName();
613
614
		return $data;
615
	}
616
617
	public function opendir($path) {
618
		try {
619
			$files = $this->getFolderContents($path);
620
		} catch (NotFoundException $e) {
621
			return false;
622
		} catch (NotPermittedException $e) {
623
			return false;
624
		}
625
		$names = array_map(function ($info) {
626
			/** @var IFileInfo $info */
627
			return $info->getName();
628
		}, iterator_to_array($files));
629
		return IteratorDirectory::wrap($names);
630
	}
631
632
	public function getDirectoryContent($directory): \Traversable {
633
		try {
634
			$files = $this->getFolderContents($directory);
635
			foreach ($files as $file) {
636
				yield $this->getMetaDataFromFileInfo($file);
637
			}
638
		} catch (NotFoundException $e) {
639
			return;
640
		} catch (NotPermittedException $e) {
641
			return;
642
		}
643
	}
644
645
	public function filetype($path) {
646
		try {
647
			return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
648
		} catch (\OCP\Files\NotFoundException $e) {
649
			return false;
650
		} catch (ForbiddenException $e) {
651
			return false;
652
		}
653
	}
654
655
	public function mkdir($path) {
656
		$path = $this->buildPath($path);
657
		try {
658
			$this->share->mkdir($path);
659
			return true;
660
		} catch (ConnectException $e) {
661
			$this->logger->logException($e, ['message' => 'Error while creating folder']);
662
			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
663
		} catch (Exception $e) {
664
			return false;
665
		}
666
	}
667
668
	public function file_exists($path) {
669
		try {
670
			$this->getFileInfo($path);
671
			return true;
672
		} catch (\OCP\Files\NotFoundException $e) {
673
			return false;
674
		} catch (ForbiddenException $e) {
675
			return false;
676
		} catch (ConnectException $e) {
677
			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
678
		}
679
	}
680
681
	public function isReadable($path) {
682
		try {
683
			$info = $this->getFileInfo($path);
684
			return $this->showHidden || !$info->isHidden();
685
		} catch (\OCP\Files\NotFoundException $e) {
686
			return false;
687
		} catch (ForbiddenException $e) {
688
			return false;
689
		}
690
	}
691
692
	public function isUpdatable($path) {
693
		try {
694
			$info = $this->getFileInfo($path);
695
			// following windows behaviour for read-only folders: they can be written into
696
			// (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
697
			return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory());
698
		} catch (\OCP\Files\NotFoundException $e) {
699
			return false;
700
		} catch (ForbiddenException $e) {
701
			return false;
702
		}
703
	}
704
705
	public function isDeletable($path) {
706
		try {
707
			$info = $this->getFileInfo($path);
708
			return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
709
		} catch (\OCP\Files\NotFoundException $e) {
710
			return false;
711
		} catch (ForbiddenException $e) {
712
			return false;
713
		}
714
	}
715
716
	/**
717
	 * check if smbclient is installed
718
	 */
719
	public static function checkDependencies() {
720
		return (
721
			(bool)\OC_Helper::findBinaryPath('smbclient')
722
			|| NativeServer::available(new System())
723
		) ? true : ['smbclient'];
724
	}
725
726
	/**
727
	 * Test a storage for availability
728
	 *
729
	 * @return bool
730
	 */
731
	public function test() {
732
		try {
733
			return parent::test();
734
		} catch (StorageAuthException $e) {
735
			return false;
736
		} catch (ForbiddenException $e) {
737
			return false;
738
		} catch (Exception $e) {
739
			$this->logger->logException($e);
740
			return false;
741
		}
742
	}
743
744
	public function listen($path, callable $callback) {
745
		$this->notify($path)->listen(function (IChange $change) use ($callback) {
746
			if ($change instanceof IRenameChange) {
747
				return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
748
			} else {
749
				return $callback($change->getType(), $change->getPath());
750
			}
751
		});
752
	}
753
754
	public function notify($path) {
755
		$path = '/' . ltrim($path, '/');
756
		$shareNotifyHandler = $this->share->notify($this->buildPath($path));
757
		return new SMBNotifyHandler($shareNotifyHandler, $this->root);
758
	}
759
}
760