Passed
Push — master ( 96d192...9c7503 )
by Morris
12:50 queued 12s
created

SMB::getFolderContents()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 27
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 20
nc 8
nop 1
dl 0
loc 27
rs 8.6666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Jesús Macias <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author Juan Pablo Villafañez <[email protected]>
9
 * @author Juan Pablo Villafáñez <[email protected]>
10
 * @author Jörn Friedrich Dreyer <[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 Thomas Müller <[email protected]>
17
 * @author Vincent Petry <[email protected]>
18
 *
19
 * @license AGPL-3.0
20
 *
21
 * This code is free software: you can redistribute it and/or modify
22
 * it under the terms of the GNU Affero General Public License, version 3,
23
 * as published by the Free Software Foundation.
24
 *
25
 * This program is distributed in the hope that it will be useful,
26
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
 * GNU Affero General Public License for more details.
29
 *
30
 * You should have received a copy of the GNU Affero General Public License, version 3,
31
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
32
 *
33
 */
34
35
namespace OCA\Files_External\Lib\Storage;
36
37
use Icewind\SMB\BasicAuth;
38
use Icewind\SMB\Exception\AlreadyExistsException;
39
use Icewind\SMB\Exception\ConnectException;
40
use Icewind\SMB\Exception\Exception;
41
use Icewind\SMB\Exception\ForbiddenException;
42
use Icewind\SMB\Exception\InvalidArgumentException;
43
use Icewind\SMB\Exception\NotFoundException;
44
use Icewind\SMB\Exception\TimedOutException;
45
use Icewind\SMB\IFileInfo;
46
use Icewind\SMB\Native\NativeServer;
47
use Icewind\SMB\ServerFactory;
48
use Icewind\SMB\System;
49
use Icewind\Streams\CallbackWrapper;
50
use Icewind\Streams\IteratorDirectory;
51
use OC\Cache\CappedMemoryCache;
52
use OC\Files\Filesystem;
53
use OC\Files\Storage\Common;
54
use OCA\Files_External\Lib\Notify\SMBNotifyHandler;
55
use OCP\Files\Notify\IChange;
56
use OCP\Files\Notify\IRenameChange;
57
use OCP\Files\Storage\INotifyStorage;
58
use OCP\Files\StorageNotAvailableException;
59
use OCP\ILogger;
60
61
class SMB extends Common implements INotifyStorage {
62
	/**
63
	 * @var \Icewind\SMB\IServer
64
	 */
65
	protected $server;
66
67
	/**
68
	 * @var \Icewind\SMB\IShare
69
	 */
70
	protected $share;
71
72
	/**
73
	 * @var string
74
	 */
75
	protected $root;
76
77
	/**
78
	 * @var \Icewind\SMB\IFileInfo[]
79
	 */
80
	protected $statCache;
81
82
	/** @var ILogger */
83
	protected $logger;
84
85
	/** @var bool */
86
	protected $showHidden;
87
88
	public function __construct($params) {
89
		if (!isset($params['host'])) {
90
			throw new \Exception('Invalid configuration, no host provided');
91
		}
92
93
		if (isset($params['auth'])) {
94
			$auth = $params['auth'];
95
		} else if (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
96
			list($workgroup, $user) = $this->splitUser($params['user']);
97
			$auth = new BasicAuth($user, $workgroup, $params['password']);
98
		} else {
99
			throw new \Exception('Invalid configuration, no credentials provided');
100
		}
101
102
		if (isset($params['logger'])) {
103
			$this->logger = $params['logger'];
104
		} else {
105
			$this->logger = \OC::$server->getLogger();
106
		}
107
108
		$serverFactory = new ServerFactory();
109
		$this->server = $serverFactory->createServer($params['host'], $auth);
110
		$this->share = $this->server->getShare(trim($params['share'], '/'));
111
112
		$this->root = $params['root'] ?? '/';
113
		$this->root = '/' . ltrim($this->root, '/');
114
		$this->root = rtrim($this->root, '/') . '/';
115
116
		$this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
117
118
		$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...
119
		parent::__construct($params);
120
	}
121
122
	private function splitUser($user) {
123
		if (strpos($user, '/')) {
124
			return explode('/', $user, 2);
125
		} elseif (strpos($user, '\\')) {
126
			return explode('\\', $user);
127
		} else {
128
			return [null, $user];
129
		}
130
	}
131
132
	/**
133
	 * @return string
134
	 */
135
	public function getId() {
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
	/**
143
	 * @param string $path
144
	 * @return string
145
	 */
146
	protected function buildPath($path) {
147
		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
148
	}
149
150
	protected function relativePath($fullPath) {
151
		if ($fullPath === $this->root) {
152
			return '';
153
		} else if (substr($fullPath, 0, strlen($this->root)) === $this->root) {
154
			return substr($fullPath, strlen($this->root));
155
		} else {
156
			return null;
157
		}
158
	}
159
160
	/**
161
	 * @param string $path
162
	 * @return \Icewind\SMB\IFileInfo
163
	 * @throws StorageNotAvailableException
164
	 */
165
	protected function getFileInfo($path) {
166
		try {
167
			$path = $this->buildPath($path);
168
			if (!isset($this->statCache[$path])) {
169
				$this->statCache[$path] = $this->share->stat($path);
170
			}
171
			return $this->statCache[$path];
172
		} catch (ConnectException $e) {
173
			$this->logger->logException($e, ['message' => 'Error while getting file info']);
174
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
175
		}
176
	}
177
178
	/**
179
	 * @param string $path
180
	 * @return \Icewind\SMB\IFileInfo[]
181
	 * @throws StorageNotAvailableException
182
	 */
183
	protected function getFolderContents($path) {
184
		try {
185
			$path = $this->buildPath($path);
186
			$files = $this->share->dir($path);
187
			foreach ($files as $file) {
188
				$this->statCache[$path . '/' . $file->getName()] = $file;
189
			}
190
			return array_filter($files, function (IFileInfo $file) {
191
				try {
192
					// the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
193
					// so we trigger the below exceptions where applicable
194
					$hide = $file->isHidden() && !$this->showHidden;
195
					if ($hide) {
196
						$this->logger->debug('hiding hidden file ' . $file->getName());
197
					}
198
					return !$hide;
199
				} catch (ForbiddenException $e) {
200
					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);
201
					return false;
202
				} catch (NotFoundException $e) {
203
					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);
204
					return false;
205
				}
206
			});
207
		} catch (ConnectException $e) {
208
			$this->logger->logException($e, ['message' => 'Error while getting folder content']);
209
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
210
		}
211
	}
212
213
	/**
214
	 * @param \Icewind\SMB\IFileInfo $info
215
	 * @return array
216
	 */
217
	protected function formatInfo($info) {
218
		$result = [
219
			'size' => $info->getSize(),
220
			'mtime' => $info->getMTime(),
221
		];
222
		if ($info->isDirectory()) {
223
			$result['type'] = 'dir';
224
		} else {
225
			$result['type'] = 'file';
226
		}
227
		return $result;
228
	}
229
230
	/**
231
	 * Rename the files. If the source or the target is the root, the rename won't happen.
232
	 *
233
	 * @param string $source the old name of the path
234
	 * @param string $target the new name of the path
235
	 * @return bool true if the rename is successful, false otherwise
236
	 */
237
	public function rename($source, $target, $retry = true) {
238
		if ($this->isRootDir($source) || $this->isRootDir($target)) {
239
			return false;
240
		}
241
242
		$absoluteSource = $this->buildPath($source);
243
		$absoluteTarget = $this->buildPath($target);
244
		try {
245
			$result = $this->share->rename($absoluteSource, $absoluteTarget);
246
		} catch (AlreadyExistsException $e) {
247
			if ($retry) {
248
				$this->remove($target);
249
				$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

249
				/** @scrutinizer ignore-call */ 
250
    $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...
250
			} else {
251
				$this->logger->logException($e, ['level' => ILogger::WARN]);
252
				return false;
253
			}
254
		} catch (InvalidArgumentException $e) {
255
			if ($retry) {
256
				$this->remove($target);
257
				$result = $this->share->rename($absoluteSource, $absoluteTarget, false);
258
			} else {
259
				$this->logger->logException($e, ['level' => ILogger::WARN]);
260
				return false;
261
			}
262
		} catch (\Exception $e) {
263
			$this->logger->logException($e, ['level' => ILogger::WARN]);
264
			return false;
265
		}
266
		unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
267
		return $result;
268
	}
269
270
	public function stat($path, $retry = true) {
271
		try {
272
			$result = $this->formatInfo($this->getFileInfo($path));
273
		} catch (ForbiddenException $e) {
274
			return false;
275
		} catch (NotFoundException $e) {
276
			return false;
277
		} catch (TimedOutException $e) {
278
			if ($retry) {
279
				return $this->stat($path, false);
280
			} else {
281
				throw $e;
282
			}
283
		}
284
		if ($this->remoteIsShare() && $this->isRootDir($path)) {
285
			$result['mtime'] = $this->shareMTime();
286
		}
287
		return $result;
288
	}
289
290
	/**
291
	 * get the best guess for the modification time of the share
292
	 *
293
	 * @return int
294
	 */
295
	private function shareMTime() {
296
		$highestMTime = 0;
297
		$files = $this->share->dir($this->root);
298
		foreach ($files as $fileInfo) {
299
			try {
300
				if ($fileInfo->getMTime() > $highestMTime) {
301
					$highestMTime = $fileInfo->getMTime();
302
				}
303
			} catch (NotFoundException $e) {
304
				// Ignore this, can happen on unavailable DFS shares
305
			}
306
		}
307
		return $highestMTime;
308
	}
309
310
	/**
311
	 * Check if the path is our root dir (not the smb one)
312
	 *
313
	 * @param string $path the path
314
	 * @return bool
315
	 */
316
	private function isRootDir($path) {
317
		return $path === '' || $path === '/' || $path === '.';
318
	}
319
320
	/**
321
	 * Check if our root points to a smb share
322
	 *
323
	 * @return bool true if our root points to a share false otherwise
324
	 */
325
	private function remoteIsShare() {
326
		return $this->share->getName() && (!$this->root || $this->root === '/');
327
	}
328
329
	/**
330
	 * @param string $path
331
	 * @return bool
332
	 */
333
	public function unlink($path) {
334
		if ($this->isRootDir($path)) {
335
			return false;
336
		}
337
338
		try {
339
			if ($this->is_dir($path)) {
340
				return $this->rmdir($path);
341
			} else {
342
				$path = $this->buildPath($path);
343
				unset($this->statCache[$path]);
344
				$this->share->del($path);
345
				return true;
346
			}
347
		} catch (NotFoundException $e) {
348
			return false;
349
		} catch (ForbiddenException $e) {
350
			return false;
351
		} catch (ConnectException $e) {
352
			$this->logger->logException($e, ['message' => 'Error while deleting file']);
353
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
354
		}
355
	}
356
357
	/**
358
	 * check if a file or folder has been updated since $time
359
	 *
360
	 * @param string $path
361
	 * @param int $time
362
	 * @return bool
363
	 */
364
	public function hasUpdated($path, $time) {
365
		if (!$path and $this->root === '/') {
366
			// mtime doesn't work for shares, but giving the nature of the backend,
367
			// doing a full update is still just fast enough
368
			return true;
369
		} else {
370
			$actualTime = $this->filemtime($path);
371
			return $actualTime > $time;
372
		}
373
	}
374
375
	/**
376
	 * @param string $path
377
	 * @param string $mode
378
	 * @return resource|false
379
	 */
380
	public function fopen($path, $mode) {
381
		$fullPath = $this->buildPath($path);
382
		try {
383
			switch ($mode) {
384
				case 'r':
385
				case 'rb':
386
					if (!$this->file_exists($path)) {
387
						return false;
388
					}
389
					return $this->share->read($fullPath);
390
				case 'w':
391
				case 'wb':
392
					$source = $this->share->write($fullPath);
393
					return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
394
						unset($this->statCache[$fullPath]);
395
					});
396
				case 'a':
397
				case 'ab':
398
				case 'r+':
399
				case 'w+':
400
				case 'wb+':
401
				case 'a+':
402
				case 'x':
403
				case 'x+':
404
				case 'c':
405
				case 'c+':
406
					//emulate these
407
					if (strrpos($path, '.') !== false) {
408
						$ext = substr($path, strrpos($path, '.'));
409
					} else {
410
						$ext = '';
411
					}
412
					if ($this->file_exists($path)) {
413
						if (!$this->isUpdatable($path)) {
414
							return false;
415
						}
416
						$tmpFile = $this->getCachedFile($path);
417
					} else {
418
						if (!$this->isCreatable(dirname($path))) {
419
							return false;
420
						}
421
						$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
422
					}
423
					$source = fopen($tmpFile, $mode);
424
					$share = $this->share;
425
					return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
426
						unset($this->statCache[$fullPath]);
427
						$share->put($tmpFile, $fullPath);
428
						unlink($tmpFile);
429
					});
430
			}
431
			return false;
432
		} catch (NotFoundException $e) {
433
			return false;
434
		} catch (ForbiddenException $e) {
435
			return false;
436
		} catch (ConnectException $e) {
437
			$this->logger->logException($e, ['message' => 'Error while opening file']);
438
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
439
		}
440
	}
441
442
	public function rmdir($path) {
443
		if ($this->isRootDir($path)) {
444
			return false;
445
		}
446
447
		try {
448
			$this->statCache = array();
449
			$content = $this->share->dir($this->buildPath($path));
450
			foreach ($content as $file) {
451
				if ($file->isDirectory()) {
452
					$this->rmdir($path . '/' . $file->getName());
453
				} else {
454
					$this->share->del($file->getPath());
455
				}
456
			}
457
			$this->share->rmdir($this->buildPath($path));
458
			return true;
459
		} catch (NotFoundException $e) {
460
			return false;
461
		} catch (ForbiddenException $e) {
462
			return false;
463
		} catch (ConnectException $e) {
464
			$this->logger->logException($e, ['message' => 'Error while removing folder']);
465
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
466
		}
467
	}
468
469
	public function touch($path, $time = null) {
470
		try {
471
			if (!$this->file_exists($path)) {
472
				$fh = $this->share->write($this->buildPath($path));
473
				fclose($fh);
474
				return true;
475
			}
476
			return false;
477
		} catch (ConnectException $e) {
478
			$this->logger->logException($e, ['message' => 'Error while creating file']);
479
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
480
		}
481
	}
482
483
	public function opendir($path) {
484
		try {
485
			$files = $this->getFolderContents($path);
486
		} catch (NotFoundException $e) {
487
			return false;
488
		} catch (ForbiddenException $e) {
489
			return false;
490
		}
491
		$names = array_map(function ($info) {
492
			/** @var \Icewind\SMB\IFileInfo $info */
493
			return $info->getName();
494
		}, $files);
495
		return IteratorDirectory::wrap($names);
496
	}
497
498
	public function filetype($path) {
499
		try {
500
			return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
501
		} catch (NotFoundException $e) {
502
			return false;
503
		} catch (ForbiddenException $e) {
504
			return false;
505
		}
506
	}
507
508
	public function mkdir($path) {
509
		$path = $this->buildPath($path);
510
		try {
511
			$this->share->mkdir($path);
512
			return true;
513
		} catch (ConnectException $e) {
514
			$this->logger->logException($e, ['message' => 'Error while creating folder']);
515
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
516
		} catch (Exception $e) {
517
			return false;
518
		}
519
	}
520
521
	public function file_exists($path) {
522
		try {
523
			$this->getFileInfo($path);
524
			return true;
525
		} catch (NotFoundException $e) {
526
			return false;
527
		} catch (ForbiddenException $e) {
528
			return false;
529
		} catch (ConnectException $e) {
530
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
531
		}
532
	}
533
534
	public function isReadable($path) {
535
		try {
536
			$info = $this->getFileInfo($path);
537
			return $this->showHidden || !$info->isHidden();
538
		} catch (NotFoundException $e) {
539
			return false;
540
		} catch (ForbiddenException $e) {
541
			return false;
542
		}
543
	}
544
545
	public function isUpdatable($path) {
546
		try {
547
			$info = $this->getFileInfo($path);
548
			// following windows behaviour for read-only folders: they can be written into
549
			// (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
550
			return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $this->is_dir($path));
551
		} catch (NotFoundException $e) {
552
			return false;
553
		} catch (ForbiddenException $e) {
554
			return false;
555
		}
556
	}
557
558
	public function isDeletable($path) {
559
		try {
560
			$info = $this->getFileInfo($path);
561
			return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
562
		} catch (NotFoundException $e) {
563
			return false;
564
		} catch (ForbiddenException $e) {
565
			return false;
566
		}
567
	}
568
569
	/**
570
	 * check if smbclient is installed
571
	 */
572
	public static function checkDependencies() {
573
		return (
574
			(bool)\OC_Helper::findBinaryPath('smbclient')
575
			|| NativeServer::available(new System())
576
		) ? true : ['smbclient'];
577
	}
578
579
	/**
580
	 * Test a storage for availability
581
	 *
582
	 * @return bool
583
	 */
584
	public function test() {
585
		try {
586
			return parent::test();
587
		} catch (Exception $e) {
588
			$this->logger->logException($e);
589
			return false;
590
		}
591
	}
592
593
	public function listen($path, callable $callback) {
594
		$this->notify($path)->listen(function (IChange $change) use ($callback) {
595
			if ($change instanceof IRenameChange) {
596
				return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
597
			} else {
598
				return $callback($change->getType(), $change->getPath());
599
			}
600
		});
601
	}
602
603
	public function notify($path) {
604
		$path = '/' . ltrim($path, '/');
605
		$shareNotifyHandler = $this->share->notify($this->buildPath($path));
606
		return new SMBNotifyHandler($shareNotifyHandler, $this->root);
607
	}
608
}
609