Passed
Push — master ( 5cdc85...37718d )
by Morris
38:53 queued 21:57
created

SMB::mkdir()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 1
dl 0
loc 10
rs 9.9666
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
	public function __construct($params) {
86
		if (!isset($params['host'])) {
87
			throw new \Exception('Invalid configuration, no host provided');
88
		}
89
90
		if (isset($params['auth'])) {
91
			$auth = $params['auth'];
92
		} else if (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
93
			list($workgroup, $user) = $this->splitUser($params['user']);
94
			$auth = new BasicAuth($user, $workgroup, $params['password']);
95
		} else {
96
			throw new \Exception('Invalid configuration, no credentials provided');
97
		}
98
99
		if (isset($params['logger'])) {
100
			$this->logger = $params['logger'];
101
		} else {
102
			$this->logger = \OC::$server->getLogger();
103
		}
104
105
		$serverFactory = new ServerFactory();
106
		$this->server = $serverFactory->createServer($params['host'], $auth);
107
		$this->share = $this->server->getShare(trim($params['share'], '/'));
108
109
		$this->root = $params['root'] ?? '/';
110
		$this->root = '/' . ltrim($this->root, '/');
111
		$this->root = rtrim($this->root, '/') . '/';
112
113
		$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...
114
		parent::__construct($params);
115
	}
116
117
	private function splitUser($user) {
118
		if (strpos($user, '/')) {
119
			return explode('/', $user, 2);
120
		} elseif (strpos($user, '\\')) {
121
			return explode('\\', $user);
122
		} else {
123
			return [null, $user];
124
		}
125
	}
126
127
	/**
128
	 * @return string
129
	 */
130
	public function getId() {
131
		// FIXME: double slash to keep compatible with the old storage ids,
132
		// failure to do so will lead to creation of a new storage id and
133
		// loss of shares from the storage
134
		return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
135
	}
136
137
	/**
138
	 * @param string $path
139
	 * @return string
140
	 */
141
	protected function buildPath($path) {
142
		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
143
	}
144
145
	protected function relativePath($fullPath) {
146
		if ($fullPath === $this->root) {
147
			return '';
148
		} else if (substr($fullPath, 0, strlen($this->root)) === $this->root) {
149
			return substr($fullPath, strlen($this->root));
150
		} else {
151
			return null;
152
		}
153
	}
154
155
	/**
156
	 * @param string $path
157
	 * @return \Icewind\SMB\IFileInfo
158
	 * @throws StorageNotAvailableException
159
	 */
160
	protected function getFileInfo($path) {
161
		try {
162
			$path = $this->buildPath($path);
163
			if (!isset($this->statCache[$path])) {
164
				$this->statCache[$path] = $this->share->stat($path);
165
			}
166
			return $this->statCache[$path];
167
		} catch (ConnectException $e) {
168
			$this->logger->logException($e, ['message' => 'Error while getting file info']);
169
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
170
		}
171
	}
172
173
	/**
174
	 * @param string $path
175
	 * @return \Icewind\SMB\IFileInfo[]
176
	 * @throws StorageNotAvailableException
177
	 */
178
	protected function getFolderContents($path) {
179
		try {
180
			$path = $this->buildPath($path);
181
			$files = $this->share->dir($path);
182
			foreach ($files as $file) {
183
				$this->statCache[$path . '/' . $file->getName()] = $file;
184
			}
185
			return array_filter($files, function (IFileInfo $file) {
186
				try {
187
					if ($file->isHidden()) {
188
						$this->logger->debug('hiding hidden file ' . $file->getName());
189
					}
190
					return !$file->isHidden();
191
				} catch (ForbiddenException $e) {
192
					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);
193
					return false;
194
				} catch (NotFoundException $e) {
195
					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);
196
					return false;
197
				}
198
			});
199
		} catch (ConnectException $e) {
200
			$this->logger->logException($e, ['message' => 'Error while getting folder content']);
201
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
202
		}
203
	}
204
205
	/**
206
	 * @param \Icewind\SMB\IFileInfo $info
207
	 * @return array
208
	 */
209
	protected function formatInfo($info) {
210
		$result = [
211
			'size' => $info->getSize(),
212
			'mtime' => $info->getMTime(),
213
		];
214
		if ($info->isDirectory()) {
215
			$result['type'] = 'dir';
216
		} else {
217
			$result['type'] = 'file';
218
		}
219
		return $result;
220
	}
221
222
	/**
223
	 * Rename the files. If the source or the target is the root, the rename won't happen.
224
	 *
225
	 * @param string $source the old name of the path
226
	 * @param string $target the new name of the path
227
	 * @return bool true if the rename is successful, false otherwise
228
	 */
229
	public function rename($source, $target, $retry = true) {
230
		if ($this->isRootDir($source) || $this->isRootDir($target)) {
231
			return false;
232
		}
233
234
		$absoluteSource = $this->buildPath($source);
235
		$absoluteTarget = $this->buildPath($target);
236
		try {
237
			$result = $this->share->rename($absoluteSource, $absoluteTarget);
238
		} catch (AlreadyExistsException $e) {
239
			if ($retry) {
240
				$this->remove($target);
241
				$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

241
				/** @scrutinizer ignore-call */ 
242
    $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...
242
			} else {
243
				$this->logger->logException($e, ['level' => ILogger::WARN]);
244
				return false;
245
			}
246
		} catch (InvalidArgumentException $e) {
247
			if ($retry) {
248
				$this->remove($target);
249
				$result = $this->share->rename($absoluteSource, $absoluteTarget, false);
250
			} else {
251
				$this->logger->logException($e, ['level' => ILogger::WARN]);
252
				return false;
253
			}
254
		} catch (\Exception $e) {
255
			$this->logger->logException($e, ['level' => ILogger::WARN]);
256
			return false;
257
		}
258
		unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
259
		return $result;
260
	}
261
262
	public function stat($path, $retry = true) {
263
		try {
264
			$result = $this->formatInfo($this->getFileInfo($path));
265
		} catch (ForbiddenException $e) {
266
			return false;
267
		} catch (NotFoundException $e) {
268
			return false;
269
		} catch (TimedOutException $e) {
270
			if ($retry) {
271
				return $this->stat($path, false);
272
			} else {
273
				throw $e;
274
			}
275
		}
276
		if ($this->remoteIsShare() && $this->isRootDir($path)) {
277
			$result['mtime'] = $this->shareMTime();
278
		}
279
		return $result;
280
	}
281
282
	/**
283
	 * get the best guess for the modification time of the share
284
	 *
285
	 * @return int
286
	 */
287
	private function shareMTime() {
288
		$highestMTime = 0;
289
		$files = $this->share->dir($this->root);
290
		foreach ($files as $fileInfo) {
291
			try {
292
				if ($fileInfo->getMTime() > $highestMTime) {
293
					$highestMTime = $fileInfo->getMTime();
294
				}
295
			} catch (NotFoundException $e) {
296
				// Ignore this, can happen on unavailable DFS shares
297
			}
298
		}
299
		return $highestMTime;
300
	}
301
302
	/**
303
	 * Check if the path is our root dir (not the smb one)
304
	 *
305
	 * @param string $path the path
306
	 * @return bool
307
	 */
308
	private function isRootDir($path) {
309
		return $path === '' || $path === '/' || $path === '.';
310
	}
311
312
	/**
313
	 * Check if our root points to a smb share
314
	 *
315
	 * @return bool true if our root points to a share false otherwise
316
	 */
317
	private function remoteIsShare() {
318
		return $this->share->getName() && (!$this->root || $this->root === '/');
319
	}
320
321
	/**
322
	 * @param string $path
323
	 * @return bool
324
	 */
325
	public function unlink($path) {
326
		if ($this->isRootDir($path)) {
327
			return false;
328
		}
329
330
		try {
331
			if ($this->is_dir($path)) {
332
				return $this->rmdir($path);
333
			} else {
334
				$path = $this->buildPath($path);
335
				unset($this->statCache[$path]);
336
				$this->share->del($path);
337
				return true;
338
			}
339
		} catch (NotFoundException $e) {
340
			return false;
341
		} catch (ForbiddenException $e) {
342
			return false;
343
		} catch (ConnectException $e) {
344
			$this->logger->logException($e, ['message' => 'Error while deleting file']);
345
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
346
		}
347
	}
348
349
	/**
350
	 * check if a file or folder has been updated since $time
351
	 *
352
	 * @param string $path
353
	 * @param int $time
354
	 * @return bool
355
	 */
356
	public function hasUpdated($path, $time) {
357
		if (!$path and $this->root === '/') {
358
			// mtime doesn't work for shares, but giving the nature of the backend,
359
			// doing a full update is still just fast enough
360
			return true;
361
		} else {
362
			$actualTime = $this->filemtime($path);
363
			return $actualTime > $time;
364
		}
365
	}
366
367
	/**
368
	 * @param string $path
369
	 * @param string $mode
370
	 * @return resource|false
371
	 */
372
	public function fopen($path, $mode) {
373
		$fullPath = $this->buildPath($path);
374
		try {
375
			switch ($mode) {
376
				case 'r':
377
				case 'rb':
378
					if (!$this->file_exists($path)) {
379
						return false;
380
					}
381
					return $this->share->read($fullPath);
382
				case 'w':
383
				case 'wb':
384
					$source = $this->share->write($fullPath);
385
					return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
386
						unset($this->statCache[$fullPath]);
387
					});
388
				case 'a':
389
				case 'ab':
390
				case 'r+':
391
				case 'w+':
392
				case 'wb+':
393
				case 'a+':
394
				case 'x':
395
				case 'x+':
396
				case 'c':
397
				case 'c+':
398
					//emulate these
399
					if (strrpos($path, '.') !== false) {
400
						$ext = substr($path, strrpos($path, '.'));
401
					} else {
402
						$ext = '';
403
					}
404
					if ($this->file_exists($path)) {
405
						if (!$this->isUpdatable($path)) {
406
							return false;
407
						}
408
						$tmpFile = $this->getCachedFile($path);
409
					} else {
410
						if (!$this->isCreatable(dirname($path))) {
411
							return false;
412
						}
413
						$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
414
					}
415
					$source = fopen($tmpFile, $mode);
416
					$share = $this->share;
417
					return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
0 ignored issues
show
Bug introduced by
It seems like $source can also be of type false; however, parameter $source of Icewind\Streams\CallbackWrapper::wrap() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

417
					return CallbackWrapper::wrap(/** @scrutinizer ignore-type */ $source, null, null, function () use ($tmpFile, $fullPath, $share) {
Loading history...
418
						unset($this->statCache[$fullPath]);
419
						$share->put($tmpFile, $fullPath);
420
						unlink($tmpFile);
421
					});
422
			}
423
			return false;
424
		} catch (NotFoundException $e) {
425
			return false;
426
		} catch (ForbiddenException $e) {
427
			return false;
428
		} catch (ConnectException $e) {
429
			$this->logger->logException($e, ['message' => 'Error while opening file']);
430
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
431
		}
432
	}
433
434
	public function rmdir($path) {
435
		if ($this->isRootDir($path)) {
436
			return false;
437
		}
438
439
		try {
440
			$this->statCache = array();
441
			$content = $this->share->dir($this->buildPath($path));
442
			foreach ($content as $file) {
443
				if ($file->isDirectory()) {
444
					$this->rmdir($path . '/' . $file->getName());
445
				} else {
446
					$this->share->del($file->getPath());
447
				}
448
			}
449
			$this->share->rmdir($this->buildPath($path));
450
			return true;
451
		} catch (NotFoundException $e) {
452
			return false;
453
		} catch (ForbiddenException $e) {
454
			return false;
455
		} catch (ConnectException $e) {
456
			$this->logger->logException($e, ['message' => 'Error while removing folder']);
457
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
458
		}
459
	}
460
461
	public function touch($path, $time = null) {
462
		try {
463
			if (!$this->file_exists($path)) {
464
				$fh = $this->share->write($this->buildPath($path));
465
				fclose($fh);
466
				return true;
467
			}
468
			return false;
469
		} catch (ConnectException $e) {
470
			$this->logger->logException($e, ['message' => 'Error while creating file']);
471
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
472
		}
473
	}
474
475
	public function opendir($path) {
476
		try {
477
			$files = $this->getFolderContents($path);
478
		} catch (NotFoundException $e) {
479
			return false;
480
		} catch (ForbiddenException $e) {
481
			return false;
482
		}
483
		$names = array_map(function ($info) {
484
			/** @var \Icewind\SMB\IFileInfo $info */
485
			return $info->getName();
486
		}, $files);
487
		return IteratorDirectory::wrap($names);
488
	}
489
490
	public function filetype($path) {
491
		try {
492
			return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
493
		} catch (NotFoundException $e) {
494
			return false;
495
		} catch (ForbiddenException $e) {
496
			return false;
497
		}
498
	}
499
500
	public function mkdir($path) {
501
		$path = $this->buildPath($path);
502
		try {
503
			$this->share->mkdir($path);
504
			return true;
505
		} catch (ConnectException $e) {
506
			$this->logger->logException($e, ['message' => 'Error while creating folder']);
507
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
508
		} catch (Exception $e) {
509
			return false;
510
		}
511
	}
512
513
	public function file_exists($path) {
514
		try {
515
			$this->getFileInfo($path);
516
			return true;
517
		} catch (NotFoundException $e) {
518
			return false;
519
		} catch (ForbiddenException $e) {
520
			return false;
521
		} catch (ConnectException $e) {
522
			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
523
		}
524
	}
525
526
	public function isReadable($path) {
527
		try {
528
			$info = $this->getFileInfo($path);
529
			return !$info->isHidden();
530
		} catch (NotFoundException $e) {
531
			return false;
532
		} catch (ForbiddenException $e) {
533
			return false;
534
		}
535
	}
536
537
	public function isUpdatable($path) {
538
		try {
539
			$info = $this->getFileInfo($path);
540
			// following windows behaviour for read-only folders: they can be written into
541
			// (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
542
			return !$info->isHidden() && (!$info->isReadOnly() || $this->is_dir($path));
543
		} catch (NotFoundException $e) {
544
			return false;
545
		} catch (ForbiddenException $e) {
546
			return false;
547
		}
548
	}
549
550
	public function isDeletable($path) {
551
		try {
552
			$info = $this->getFileInfo($path);
553
			return !$info->isHidden() && !$info->isReadOnly();
554
		} catch (NotFoundException $e) {
555
			return false;
556
		} catch (ForbiddenException $e) {
557
			return false;
558
		}
559
	}
560
561
	/**
562
	 * check if smbclient is installed
563
	 */
564
	public static function checkDependencies() {
565
		return (
566
			(bool)\OC_Helper::findBinaryPath('smbclient')
567
			|| NativeServer::available(new System())
568
		) ? true : ['smbclient'];
569
	}
570
571
	/**
572
	 * Test a storage for availability
573
	 *
574
	 * @return bool
575
	 */
576
	public function test() {
577
		try {
578
			return parent::test();
579
		} catch (Exception $e) {
580
			$this->logger->logException($e);
581
			return false;
582
		}
583
	}
584
585
	public function listen($path, callable $callback) {
586
		$this->notify($path)->listen(function (IChange $change) use ($callback) {
587
			if ($change instanceof IRenameChange) {
588
				return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
589
			} else {
590
				return $callback($change->getType(), $change->getPath());
591
			}
592
		});
593
	}
594
595
	public function notify($path) {
596
		$path = '/' . ltrim($path, '/');
597
		$shareNotifyHandler = $this->share->notify($this->buildPath($path));
598
		return new SMBNotifyHandler($shareNotifyHandler, $this->root);
599
	}
600
}
601