Passed
Push — master ( e6fd9b...fb09f3 )
by Morris
16:50 queued 10s
created

SMB::getMetaDataFromFileInfo()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 33
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 7
eloc 23
c 1
b 0
f 1
nc 12
nop 1
dl 0
loc 33
rs 8.6186
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 Michael Gapczynski <[email protected]>
12
 * @author Morris Jobke <[email protected]>
13
 * @author Philipp Kapfer <[email protected]>
14
 * @author Robin Appelman <[email protected]>
15
 * @author Robin McCorkell <[email protected]>
16
 * @author Roeland Jago Douma <[email protected]>
17
 * @author Roland Tapken <[email protected]>
18
 * @author Thomas Müller <[email protected]>
19
 * @author Vincent Petry <[email protected]>
20
 *
21
 * @license AGPL-3.0
22
 *
23
 * This code is free software: you can redistribute it and/or modify
24
 * it under the terms of the GNU Affero General Public License, version 3,
25
 * as published by the Free Software Foundation.
26
 *
27
 * This program is distributed in the hope that it will be useful,
28
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
29
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30
 * GNU Affero General Public License for more details.
31
 *
32
 * You should have received a copy of the GNU Affero General Public License, version 3,
33
 * along with this program. If not, see <http://www.gnu.org/licenses/>
34
 *
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\NotFoundException;
47
use Icewind\SMB\Exception\OutOfSpaceException;
48
use Icewind\SMB\Exception\TimedOutException;
49
use Icewind\SMB\IFileInfo;
50
use Icewind\SMB\Native\NativeServer;
51
use Icewind\SMB\Options;
52
use Icewind\SMB\ServerFactory;
53
use Icewind\SMB\System;
54
use Icewind\Streams\CallbackWrapper;
55
use Icewind\Streams\IteratorDirectory;
56
use OC\Cache\CappedMemoryCache;
57
use OC\Files\Filesystem;
58
use OC\Files\Storage\Common;
59
use OCA\Files_External\Lib\Notify\SMBNotifyHandler;
60
use OCP\Constants;
61
use OCP\Files\EntityTooLargeException;
62
use OCP\Files\Notify\IChange;
63
use OCP\Files\Notify\IRenameChange;
64
use OCP\Files\NotPermittedException;
65
use OCP\Files\Storage\INotifyStorage;
66
use OCP\Files\StorageAuthException;
67
use OCP\Files\StorageNotAvailableException;
68
use OCP\ILogger;
69
70
class SMB extends Common implements INotifyStorage {
71
	/**
72
	 * @var \Icewind\SMB\IServer
73
	 */
74
	protected $server;
75
76
	/**
77
	 * @var \Icewind\SMB\IShare
78
	 */
79
	protected $share;
80
81
	/**
82
	 * @var string
83
	 */
84
	protected $root;
85
86
	/**
87
	 * @var \Icewind\SMB\IFileInfo[]
88
	 */
89
	protected $statCache;
90
91
	/** @var ILogger */
92
	protected $logger;
93
94
	/** @var bool */
95
	protected $showHidden;
96
97
	/** @var bool */
98
	protected $checkAcl;
99
100
	/** @var bool */
101
	protected $rootWritable;
102
103
	public function __construct($params) {
104
		if (!isset($params['host'])) {
105
			throw new \Exception('Invalid configuration, no host provided');
106
		}
107
108
		if (isset($params['auth'])) {
109
			$auth = $params['auth'];
110
		} elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
111
			[$workgroup, $user] = $this->splitUser($params['user']);
112
			$auth = new BasicAuth($user, $workgroup, $params['password']);
113
		} else {
114
			throw new \Exception('Invalid configuration, no credentials provided');
115
		}
116
117
		if (isset($params['logger'])) {
118
			$this->logger = $params['logger'];
119
		} else {
120
			$this->logger = \OC::$server->getLogger();
121
		}
122
123
		$options = new Options();
124
		if (isset($params['timeout'])) {
125
			$timeout = (int)$params['timeout'];
126
			if ($timeout > 0) {
127
				$options->setTimeout($timeout);
128
			}
129
		}
130
		$serverFactory = new ServerFactory($options);
131
		$this->server = $serverFactory->createServer($params['host'], $auth);
132
		$this->share = $this->server->getShare(trim($params['share'], '/'));
133
134
		$this->root = $params['root'] ?? '/';
135
		$this->root = '/' . ltrim($this->root, '/');
136
		$this->root = rtrim($this->root, '/') . '/';
137
138
		$this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
139
		$this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
140
		$this->rootWritable = isset($params['root_force_writable']) && $params['root_force_writable'];
141
142
		$this->statCache = new CappedMemoryCache();
0 ignored issues
show
Documentation Bug introduced by
It seems like new OC\Cache\CappedMemoryCache() of type OC\Cache\CappedMemoryCache is incompatible with the declared type Icewind\SMB\IFileInfo[] of property $statCache.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
143
		parent::__construct($params);
144
	}
145
146
	private function splitUser($user) {
147
		if (strpos($user, '/')) {
148
			return explode('/', $user, 2);
149
		} elseif (strpos($user, '\\')) {
150
			return explode('\\', $user);
151
		} else {
152
			return [null, $user];
153
		}
154
	}
155
156
	/**
157
	 * @return string
158
	 */
159
	public function getId() {
160
		// FIXME: double slash to keep compatible with the old storage ids,
161
		// failure to do so will lead to creation of a new storage id and
162
		// loss of shares from the storage
163
		return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
164
	}
165
166
	/**
167
	 * @param string $path
168
	 * @return string
169
	 */
170
	protected function buildPath($path) {
171
		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
172
	}
173
174
	protected function relativePath($fullPath) {
175
		if ($fullPath === $this->root) {
176
			return '';
177
		} elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
178
			return substr($fullPath, strlen($this->root));
179
		} else {
180
			return null;
181
		}
182
	}
183
184
	/**
185
	 * @param string $path
186
	 * @return \Icewind\SMB\IFileInfo
187
	 * @throws StorageAuthException
188
	 */
189
	protected function getFileInfo($path) {
190
		try {
191
			$path = $this->buildPath($path);
192
			if (!isset($this->statCache[$path])) {
193
				$this->statCache[$path] = $this->share->stat($path);
194
			}
195
			return $this->statCache[$path];
196
		} catch (ConnectException $e) {
197
			$this->throwUnavailable($e);
198
		} catch (ForbiddenException $e) {
199
			// with php-smbclient, this exceptions is thrown when the provided password is invalid.
200
			// Possible is also ForbiddenException with a different error code, so we check it.
201
			if ($e->getCode() === 1) {
202
				$this->throwUnavailable($e);
203
			}
204
			throw $e;
205
		}
206
	}
207
208
	/**
209
	 * @param \Exception $e
210
	 * @throws StorageAuthException
211
	 */
212
	protected function throwUnavailable(\Exception $e) {
213
		$this->logger->logException($e, ['message' => 'Error while getting file info']);
214
		throw new StorageAuthException($e->getMessage(), $e);
215
	}
216
217
	/**
218
	 * get the acl from fileinfo that is relevant for the configured user
219
	 *
220
	 * @param IFileInfo $file
221
	 * @return ACL|null
222
	 */
223
	private function getACL(IFileInfo $file): ?ACL {
224
		$acls = $file->getAcls();
225
		foreach ($acls as $user => $acl) {
226
			[, $user] = explode('\\', $user); // strip domain
227
			if ($user === $this->server->getAuth()->getUsername()) {
228
				return $acl;
229
			}
230
		}
231
232
		return null;
233
	}
234
235
	/**
236
	 * @param string $path
237
	 * @return \Icewind\SMB\IFileInfo[]
238
	 * @throws StorageNotAvailableException
239
	 */
240
	protected function getFolderContents($path): iterable {
241
		try {
242
			$path = ltrim($this->buildPath($path), '/');
243
			try {
244
				$files = $this->share->dir($path);
245
			} catch (ForbiddenException $e) {
246
				throw new NotPermittedException();
247
			}
248
			foreach ($files as $file) {
249
				$this->statCache[$path . '/' . $file->getName()] = $file;
250
			}
251
252
			foreach ($files as $file) {
253
				try {
254
					// the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
255
					// so we trigger the below exceptions where applicable
256
					$hide = $file->isHidden() && !$this->showHidden;
257
258
					if ($this->checkAcl && $acl = $this->getACL($file)) {
259
						// if there is no explicit deny, we assume it's allowed
260
						// this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
261
						// additionally, it's better to have false negatives here then false positives
262
						if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
263
							$this->logger->debug('Hiding non readable entry ' . $file->getName());
264
							return false;
265
						}
266
					}
267
268
					if ($hide) {
269
						$this->logger->debug('hiding hidden file ' . $file->getName());
270
					}
271
					if (!$hide) {
272
						yield $file;
0 ignored issues
show
Bug Best Practice introduced by
The expression yield $file returns the type Generator which is incompatible with the documented return type Icewind\SMB\IFileInfo[].
Loading history...
273
					}
274
				} catch (ForbiddenException $e) {
275
					$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

275
					$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...
276
				} catch (NotFoundException $e) {
277
					$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

277
					$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...
278
				}
279
			}
280
		} catch (ConnectException $e) {
281
			$this->logger->logException($e, ['message' => 'Error while getting folder content']);
282
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
283
		}
284
	}
285
286
	/**
287
	 * @param \Icewind\SMB\IFileInfo $info
288
	 * @return array
289
	 */
290
	protected function formatInfo($info) {
291
		$result = [
292
			'size' => $info->getSize(),
293
			'mtime' => $info->getMTime(),
294
		];
295
		if ($info->isDirectory()) {
296
			$result['type'] = 'dir';
297
		} else {
298
			$result['type'] = 'file';
299
		}
300
		return $result;
301
	}
302
303
	/**
304
	 * Rename the files. If the source or the target is the root, the rename won't happen.
305
	 *
306
	 * @param string $source the old name of the path
307
	 * @param string $target the new name of the path
308
	 * @return bool true if the rename is successful, false otherwise
309
	 */
310
	public function rename($source, $target, $retry = true) {
311
		if ($this->isRootDir($source) || $this->isRootDir($target)) {
312
			return false;
313
		}
314
315
		$absoluteSource = $this->buildPath($source);
316
		$absoluteTarget = $this->buildPath($target);
317
		try {
318
			$result = $this->share->rename($absoluteSource, $absoluteTarget);
319
		} catch (AlreadyExistsException $e) {
320
			if ($retry) {
321
				$this->remove($target);
322
				$result = $this->share->rename($absoluteSource, $absoluteTarget, false);
0 ignored issues
show
Unused Code introduced by
The call to Icewind\SMB\IShare::rename() has too many arguments starting with false. ( Ignorable by Annotation )

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

322
				/** @scrutinizer ignore-call */ 
323
    $result = $this->share->rename($absoluteSource, $absoluteTarget, false);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
323
			} else {
324
				$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

324
				$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...
325
				return false;
326
			}
327
		} catch (InvalidArgumentException $e) {
328
			if ($retry) {
329
				$this->remove($target);
330
				$result = $this->share->rename($absoluteSource, $absoluteTarget, false);
331
			} else {
332
				$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

332
				$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...
333
				return false;
334
			}
335
		} catch (\Exception $e) {
336
			$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

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