Passed
Push — master ( 08209e...071a38 )
by Roeland
41:40 queued 26:56
created

SMB::isUpdatable()   A

Complexity

Conditions 6
Paths 13

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 7
c 1
b 0
f 0
nc 13
nop 1
dl 0
loc 10
rs 9.2222
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
	public function __construct($params) {
101
		if (!isset($params['host'])) {
102
			throw new \Exception('Invalid configuration, no host provided');
103
		}
104
105
		if (isset($params['auth'])) {
106
			$auth = $params['auth'];
107
		} elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
108
			[$workgroup, $user] = $this->splitUser($params['user']);
109
			$auth = new BasicAuth($user, $workgroup, $params['password']);
110
		} else {
111
			throw new \Exception('Invalid configuration, no credentials provided');
112
		}
113
114
		if (isset($params['logger'])) {
115
			$this->logger = $params['logger'];
116
		} else {
117
			$this->logger = \OC::$server->getLogger();
118
		}
119
120
		$options = new Options();
121
		if (isset($params['timeout'])) {
122
			$timeout = (int)$params['timeout'];
123
			if ($timeout > 0) {
124
				$options->setTimeout($timeout);
125
			}
126
		}
127
		$serverFactory = new ServerFactory($options);
128
		$this->server = $serverFactory->createServer($params['host'], $auth);
129
		$this->share = $this->server->getShare(trim($params['share'], '/'));
130
131
		$this->root = $params['root'] ?? '/';
132
		$this->root = '/' . ltrim($this->root, '/');
133
		$this->root = rtrim($this->root, '/') . '/';
134
135
		$this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
136
		$this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
137
138
		$this->statCache = new CappedMemoryCache();
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...
139
		parent::__construct($params);
140
	}
141
142
	private function splitUser($user) {
143
		if (strpos($user, '/')) {
144
			return explode('/', $user, 2);
145
		} elseif (strpos($user, '\\')) {
146
			return explode('\\', $user);
147
		} else {
148
			return [null, $user];
149
		}
150
	}
151
152
	/**
153
	 * @return string
154
	 */
155
	public function getId() {
156
		// FIXME: double slash to keep compatible with the old storage ids,
157
		// failure to do so will lead to creation of a new storage id and
158
		// loss of shares from the storage
159
		return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
160
	}
161
162
	/**
163
	 * @param string $path
164
	 * @return string
165
	 */
166
	protected function buildPath($path) {
167
		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
168
	}
169
170
	protected function relativePath($fullPath) {
171
		if ($fullPath === $this->root) {
172
			return '';
173
		} elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
174
			return substr($fullPath, strlen($this->root));
175
		} else {
176
			return null;
177
		}
178
	}
179
180
	/**
181
	 * @param string $path
182
	 * @return \Icewind\SMB\IFileInfo
183
	 * @throws StorageAuthException
184
	 */
185
	protected function getFileInfo($path) {
186
		try {
187
			$path = $this->buildPath($path);
188
			if (!isset($this->statCache[$path])) {
189
				$this->statCache[$path] = $this->share->stat($path);
190
			}
191
			return $this->statCache[$path];
192
		} catch (ConnectException $e) {
193
			$this->throwUnavailable($e);
194
		} catch (ForbiddenException $e) {
195
			// with php-smbclient, this exceptions is thrown when the provided password is invalid.
196
			// Possible is also ForbiddenException with a different error code, so we check it.
197
			if ($e->getCode() === 1) {
198
				$this->throwUnavailable($e);
199
			}
200
			throw $e;
201
		}
202
	}
203
204
	/**
205
	 * @param \Exception $e
206
	 * @throws StorageAuthException
207
	 */
208
	protected function throwUnavailable(\Exception $e) {
209
		$this->logger->logException($e, ['message' => 'Error while getting file info']);
210
		throw new StorageAuthException($e->getMessage(), $e);
211
	}
212
213
	/**
214
	 * get the acl from fileinfo that is relevant for the configured user
215
	 *
216
	 * @param IFileInfo $file
217
	 * @return ACL|null
218
	 */
219
	private function getACL(IFileInfo $file): ?ACL {
220
		$acls = $file->getAcls();
221
		foreach ($acls as $user => $acl) {
222
			[, $user] = explode('\\', $user); // strip domain
223
			if ($user === $this->server->getAuth()->getUsername()) {
224
				return $acl;
225
			}
226
		}
227
228
		return null;
229
	}
230
231
	/**
232
	 * @param string $path
233
	 * @return \Icewind\SMB\IFileInfo[]
234
	 * @throws StorageNotAvailableException
235
	 */
236
	protected function getFolderContents($path): iterable {
237
		try {
238
			$path = ltrim($this->buildPath($path), '/');
239
			try {
240
				$files = $this->share->dir($path);
241
			} catch (ForbiddenException $e) {
242
				throw new NotPermittedException();
243
			}
244
			foreach ($files as $file) {
245
				$this->statCache[$path . '/' . $file->getName()] = $file;
246
			}
247
248
			foreach ($files as $file) {
249
				try {
250
					// the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
251
					// so we trigger the below exceptions where applicable
252
					$hide = $file->isHidden() && !$this->showHidden;
253
254
					if ($this->checkAcl && $acl = $this->getACL($file)) {
255
						// if there is no explicit deny, we assume it's allowed
256
						// this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
257
						// additionally, it's better to have false negatives here then false positives
258
						if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
259
							$this->logger->debug('Hiding non readable entry ' . $file->getName());
260
							return false;
261
						}
262
					}
263
264
					if ($hide) {
265
						$this->logger->debug('hiding hidden file ' . $file->getName());
266
					}
267
					if (!$hide) {
268
						yield $file;
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...
269
					}
270
				} catch (ForbiddenException $e) {
271
					$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

271
					$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...
272
				} catch (NotFoundException $e) {
273
					$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

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

318
				/** @scrutinizer ignore-call */ 
319
    $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...
319
			} else {
320
				$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

320
				$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...
321
				return false;
322
			}
323
		} catch (InvalidArgumentException $e) {
324
			if ($retry) {
325
				$this->remove($target);
326
				$result = $this->share->rename($absoluteSource, $absoluteTarget, false);
327
			} else {
328
				$this->logger->logException($e, ['level' => ILogger::WARN]);
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

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