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