Passed
Push — master ( 72fce8...03e965 )
by Robin
14:55 queued 13s
created
lib/private/Files/Cache/Scanner.php 1 patch
Indentation   +510 added lines, -510 removed lines patch added patch discarded remove patch
@@ -58,514 +58,514 @@
 block discarded – undo
58 58
  * @package OC\Files\Cache
59 59
  */
60 60
 class Scanner extends BasicEmitter implements IScanner {
61
-	/**
62
-	 * @var \OC\Files\Storage\Storage $storage
63
-	 */
64
-	protected $storage;
65
-
66
-	/**
67
-	 * @var string $storageId
68
-	 */
69
-	protected $storageId;
70
-
71
-	/**
72
-	 * @var \OC\Files\Cache\Cache $cache
73
-	 */
74
-	protected $cache;
75
-
76
-	/**
77
-	 * @var boolean $cacheActive If true, perform cache operations, if false, do not affect cache
78
-	 */
79
-	protected $cacheActive;
80
-
81
-	/**
82
-	 * @var bool $useTransactions whether to use transactions
83
-	 */
84
-	protected $useTransactions = true;
85
-
86
-	/**
87
-	 * @var \OCP\Lock\ILockingProvider
88
-	 */
89
-	protected $lockingProvider;
90
-
91
-	public function __construct(\OC\Files\Storage\Storage $storage) {
92
-		$this->storage = $storage;
93
-		$this->storageId = $this->storage->getId();
94
-		$this->cache = $storage->getCache();
95
-		$this->cacheActive = !\OC::$server->getConfig()->getSystemValue('filesystem_cache_readonly', false);
96
-		$this->lockingProvider = \OC::$server->getLockingProvider();
97
-	}
98
-
99
-	/**
100
-	 * Whether to wrap the scanning of a folder in a database transaction
101
-	 * On default transactions are used
102
-	 *
103
-	 * @param bool $useTransactions
104
-	 */
105
-	public function setUseTransactions($useTransactions) {
106
-		$this->useTransactions = $useTransactions;
107
-	}
108
-
109
-	/**
110
-	 * get all the metadata of a file or folder
111
-	 * *
112
-	 *
113
-	 * @param string $path
114
-	 * @return array|null an array of metadata of the file
115
-	 */
116
-	protected function getData($path) {
117
-		$data = $this->storage->getMetaData($path);
118
-		if (is_null($data)) {
119
-			\OC::$server->get(LoggerInterface::class)->debug("!!! Path '$path' is not accessible or present !!!", ['app' => 'core']);
120
-		}
121
-		return $data;
122
-	}
123
-
124
-	/**
125
-	 * scan a single file and store it in the cache
126
-	 *
127
-	 * @param string $file
128
-	 * @param int $reuseExisting
129
-	 * @param int $parentId
130
-	 * @param array|null|false $cacheData existing data in the cache for the file to be scanned
131
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
132
-	 * @param null $data the metadata for the file, as returned by the storage
133
-	 * @return array|null an array of metadata of the scanned file
134
-	 * @throws \OCP\Lock\LockedException
135
-	 */
136
-	public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
137
-		if ($file !== '') {
138
-			try {
139
-				$this->storage->verifyPath(dirname($file), basename($file));
140
-			} catch (\Exception $e) {
141
-				return null;
142
-			}
143
-		}
144
-		// only proceed if $file is not a partial file, blacklist is handled by the storage
145
-		if (!self::isPartialFile($file)) {
146
-			// acquire a lock
147
-			if ($lock) {
148
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
149
-					$this->storage->acquireLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
150
-				}
151
-			}
152
-
153
-			try {
154
-				$data = $data ?? $this->getData($file);
155
-			} catch (ForbiddenException $e) {
156
-				if ($lock) {
157
-					if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
158
-						$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
159
-					}
160
-				}
161
-
162
-				return null;
163
-			}
164
-
165
-			try {
166
-				if ($data) {
167
-					// pre-emit only if it was a file. By that we avoid counting/treating folders as files
168
-					if ($data['mimetype'] !== 'httpd/unix-directory') {
169
-						$this->emit('\OC\Files\Cache\Scanner', 'scanFile', [$file, $this->storageId]);
170
-						\OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', ['path' => $file, 'storage' => $this->storageId]);
171
-					}
172
-
173
-					$parent = dirname($file);
174
-					if ($parent === '.' || $parent === '/') {
175
-						$parent = '';
176
-					}
177
-					if ($parentId === -1) {
178
-						$parentId = $this->cache->getParentId($file);
179
-					}
180
-
181
-					// scan the parent if it's not in the cache (id -1) and the current file is not the root folder
182
-					if ($file && $parentId === -1) {
183
-						$parentData = $this->scanFile($parent);
184
-						if (!$parentData) {
185
-							return null;
186
-						}
187
-						$parentId = $parentData['fileid'];
188
-					}
189
-					if ($parent) {
190
-						$data['parent'] = $parentId;
191
-					}
192
-					if (is_null($cacheData)) {
193
-						/** @var CacheEntry $cacheData */
194
-						$cacheData = $this->cache->get($file);
195
-					}
196
-					if ($cacheData && $reuseExisting && isset($cacheData['fileid'])) {
197
-						// prevent empty etag
198
-						$etag = empty($cacheData['etag']) ? $data['etag'] : $cacheData['etag'];
199
-						$fileId = $cacheData['fileid'];
200
-						$data['fileid'] = $fileId;
201
-						// only reuse data if the file hasn't explicitly changed
202
-						if (isset($data['storage_mtime']) && isset($cacheData['storage_mtime']) && $data['storage_mtime'] === $cacheData['storage_mtime']) {
203
-							$data['mtime'] = $cacheData['mtime'];
204
-							if (($reuseExisting & self::REUSE_SIZE) && ($data['size'] === -1)) {
205
-								$data['size'] = $cacheData['size'];
206
-							}
207
-							if ($reuseExisting & self::REUSE_ETAG && !$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
208
-								$data['etag'] = $etag;
209
-							}
210
-						}
211
-						// Only update metadata that has changed
212
-						$newData = array_diff_assoc($data, $cacheData->getData());
213
-					} else {
214
-						$newData = $data;
215
-						$fileId = -1;
216
-					}
217
-					if (!empty($newData)) {
218
-						// Reset the checksum if the data has changed
219
-						$newData['checksum'] = '';
220
-						$newData['parent'] = $parentId;
221
-						$data['fileid'] = $this->addToCache($file, $newData, $fileId);
222
-					}
223
-
224
-					$data['oldSize'] = ($cacheData && isset($cacheData['size'])) ? $cacheData['size'] : 0;
225
-
226
-					if ($cacheData && isset($cacheData['encrypted'])) {
227
-						$data['encrypted'] = $cacheData['encrypted'];
228
-					}
229
-
230
-					// post-emit only if it was a file. By that we avoid counting/treating folders as files
231
-					if ($data['mimetype'] !== 'httpd/unix-directory') {
232
-						$this->emit('\OC\Files\Cache\Scanner', 'postScanFile', [$file, $this->storageId]);
233
-						\OC_Hook::emit('\OC\Files\Cache\Scanner', 'post_scan_file', ['path' => $file, 'storage' => $this->storageId]);
234
-					}
235
-				} else {
236
-					$this->removeFromCache($file);
237
-				}
238
-			} catch (\Exception $e) {
239
-				if ($lock) {
240
-					if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
241
-						$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
242
-					}
243
-				}
244
-				throw $e;
245
-			}
246
-
247
-			// release the acquired lock
248
-			if ($lock) {
249
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
250
-					$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
251
-				}
252
-			}
253
-
254
-			if ($data && !isset($data['encrypted'])) {
255
-				$data['encrypted'] = false;
256
-			}
257
-			return $data;
258
-		}
259
-
260
-		return null;
261
-	}
262
-
263
-	protected function removeFromCache($path) {
264
-		\OC_Hook::emit('Scanner', 'removeFromCache', ['file' => $path]);
265
-		$this->emit('\OC\Files\Cache\Scanner', 'removeFromCache', [$path]);
266
-		if ($this->cacheActive) {
267
-			$this->cache->remove($path);
268
-		}
269
-	}
270
-
271
-	/**
272
-	 * @param string $path
273
-	 * @param array $data
274
-	 * @param int $fileId
275
-	 * @return int the id of the added file
276
-	 */
277
-	protected function addToCache($path, $data, $fileId = -1) {
278
-		if (isset($data['scan_permissions'])) {
279
-			$data['permissions'] = $data['scan_permissions'];
280
-		}
281
-		\OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
282
-		$this->emit('\OC\Files\Cache\Scanner', 'addToCache', [$path, $this->storageId, $data]);
283
-		if ($this->cacheActive) {
284
-			if ($fileId !== -1) {
285
-				$this->cache->update($fileId, $data);
286
-				return $fileId;
287
-			} else {
288
-				return $this->cache->insert($path, $data);
289
-			}
290
-		} else {
291
-			return -1;
292
-		}
293
-	}
294
-
295
-	/**
296
-	 * @param string $path
297
-	 * @param array $data
298
-	 * @param int $fileId
299
-	 */
300
-	protected function updateCache($path, $data, $fileId = -1) {
301
-		\OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
302
-		$this->emit('\OC\Files\Cache\Scanner', 'updateCache', [$path, $this->storageId, $data]);
303
-		if ($this->cacheActive) {
304
-			if ($fileId !== -1) {
305
-				$this->cache->update($fileId, $data);
306
-			} else {
307
-				$this->cache->put($path, $data);
308
-			}
309
-		}
310
-	}
311
-
312
-	/**
313
-	 * scan a folder and all it's children
314
-	 *
315
-	 * @param string $path
316
-	 * @param bool $recursive
317
-	 * @param int $reuse
318
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
319
-	 * @return array|null an array of the meta data of the scanned file or folder
320
-	 */
321
-	public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
322
-		if ($reuse === -1) {
323
-			$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
324
-		}
325
-		if ($lock) {
326
-			if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
327
-				$this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
328
-				$this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
329
-			}
330
-		}
331
-		try {
332
-			try {
333
-				$data = $this->scanFile($path, $reuse, -1, null, $lock);
334
-				if ($data && $data['mimetype'] === 'httpd/unix-directory') {
335
-					$size = $this->scanChildren($path, $recursive, $reuse, $data['fileid'], $lock, $data);
336
-					$data['size'] = $size;
337
-				}
338
-			} catch (NotFoundException $e) {
339
-				$this->removeFromCache($path);
340
-				return null;
341
-			}
342
-		} finally {
343
-			if ($lock) {
344
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
345
-					$this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
346
-					$this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
347
-				}
348
-			}
349
-		}
350
-		return $data;
351
-	}
352
-
353
-	/**
354
-	 * Get the children currently in the cache
355
-	 *
356
-	 * @param int $folderId
357
-	 * @return array[]
358
-	 */
359
-	protected function getExistingChildren($folderId) {
360
-		$existingChildren = [];
361
-		$children = $this->cache->getFolderContentsById($folderId);
362
-		foreach ($children as $child) {
363
-			$existingChildren[$child['name']] = $child;
364
-		}
365
-		return $existingChildren;
366
-	}
367
-
368
-	/**
369
-	 * scan all the files and folders in a folder
370
-	 *
371
-	 * @param string $path
372
-	 * @param bool $recursive
373
-	 * @param int $reuse
374
-	 * @param int $folderId id for the folder to be scanned
375
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
376
-	 * @param array $data the data of the folder before (re)scanning the children
377
-	 * @return int the size of the scanned folder or -1 if the size is unknown at this stage
378
-	 */
379
-	protected function scanChildren($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $folderId = null, $lock = true, array $data = []) {
380
-		if ($reuse === -1) {
381
-			$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
382
-		}
383
-		$this->emit('\OC\Files\Cache\Scanner', 'scanFolder', [$path, $this->storageId]);
384
-		$size = 0;
385
-		if (!is_null($folderId)) {
386
-			$folderId = $this->cache->getId($path);
387
-		}
388
-		$childQueue = $this->handleChildren($path, $recursive, $reuse, $folderId, $lock, $size);
389
-
390
-		foreach ($childQueue as $child => $childId) {
391
-			$childSize = $this->scanChildren($child, $recursive, $reuse, $childId, $lock);
392
-			if ($childSize === -1) {
393
-				$size = -1;
394
-			} elseif ($size !== -1) {
395
-				$size += $childSize;
396
-			}
397
-		}
398
-		$oldSize = $data['size'] ?? null;
399
-		if ($this->cacheActive && $oldSize !== $size) {
400
-			$this->cache->update($folderId, ['size' => $size]);
401
-		}
402
-		$this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', [$path, $this->storageId]);
403
-		return $size;
404
-	}
405
-
406
-	private function handleChildren($path, $recursive, $reuse, $folderId, $lock, &$size) {
407
-		// we put this in it's own function so it cleans up the memory before we start recursing
408
-		$existingChildren = $this->getExistingChildren($folderId);
409
-		$newChildren = iterator_to_array($this->storage->getDirectoryContent($path));
410
-
411
-		if (count($existingChildren) === 0 && count($newChildren) === 0) {
412
-			// no need to do a transaction
413
-			return [];
414
-		}
415
-
416
-		if ($this->useTransactions) {
417
-			\OC::$server->getDatabaseConnection()->beginTransaction();
418
-		}
419
-
420
-		$exceptionOccurred = false;
421
-		$childQueue = [];
422
-		$newChildNames = [];
423
-		foreach ($newChildren as $fileMeta) {
424
-			$permissions = isset($fileMeta['scan_permissions']) ? $fileMeta['scan_permissions'] : $fileMeta['permissions'];
425
-			if ($permissions === 0) {
426
-				continue;
427
-			}
428
-			$originalFile = $fileMeta['name'];
429
-			$file = trim(\OC\Files\Filesystem::normalizePath($originalFile), '/');
430
-			if (trim($originalFile, '/') !== $file) {
431
-				// encoding mismatch, might require compatibility wrapper
432
-				\OC::$server->get(LoggerInterface::class)->debug('Scanner: Skipping non-normalized file name "'. $originalFile . '" in path "' . $path . '".', ['app' => 'core']);
433
-				$this->emit('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', [$path ? $path . '/' . $originalFile : $originalFile]);
434
-				// skip this entry
435
-				continue;
436
-			}
437
-
438
-			$newChildNames[] = $file;
439
-			$child = $path ? $path . '/' . $file : $file;
440
-			try {
441
-				$existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : false;
442
-				$data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock, $fileMeta);
443
-				if ($data) {
444
-					if ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE) {
445
-						$childQueue[$child] = $data['fileid'];
446
-					} elseif ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE_INCOMPLETE && $data['size'] === -1) {
447
-						// only recurse into folders which aren't fully scanned
448
-						$childQueue[$child] = $data['fileid'];
449
-					} elseif ($data['size'] === -1) {
450
-						$size = -1;
451
-					} elseif ($size !== -1) {
452
-						$size += $data['size'];
453
-					}
454
-				}
455
-			} catch (Exception $ex) {
456
-				// might happen if inserting duplicate while a scanning
457
-				// process is running in parallel
458
-				// log and ignore
459
-				if ($this->useTransactions) {
460
-					\OC::$server->getDatabaseConnection()->rollback();
461
-					\OC::$server->getDatabaseConnection()->beginTransaction();
462
-				}
463
-				\OC::$server->get(LoggerInterface::class)->debug('Exception while scanning file "' . $child . '"', [
464
-					'app' => 'core',
465
-					'exception' => $ex,
466
-				]);
467
-				$exceptionOccurred = true;
468
-			} catch (\OCP\Lock\LockedException $e) {
469
-				if ($this->useTransactions) {
470
-					\OC::$server->getDatabaseConnection()->rollback();
471
-				}
472
-				throw $e;
473
-			}
474
-		}
475
-		$removedChildren = \array_diff(array_keys($existingChildren), $newChildNames);
476
-		foreach ($removedChildren as $childName) {
477
-			$child = $path ? $path . '/' . $childName : $childName;
478
-			$this->removeFromCache($child);
479
-		}
480
-		if ($this->useTransactions) {
481
-			\OC::$server->getDatabaseConnection()->commit();
482
-		}
483
-		if ($exceptionOccurred) {
484
-			// It might happen that the parallel scan process has already
485
-			// inserted mimetypes but those weren't available yet inside the transaction
486
-			// To make sure to have the updated mime types in such cases,
487
-			// we reload them here
488
-			\OC::$server->getMimeTypeLoader()->reset();
489
-		}
490
-		return $childQueue;
491
-	}
492
-
493
-	/**
494
-	 * check if the file should be ignored when scanning
495
-	 * NOTE: files with a '.part' extension are ignored as well!
496
-	 *       prevents unfinished put requests to be scanned
497
-	 *
498
-	 * @param string $file
499
-	 * @return boolean
500
-	 */
501
-	public static function isPartialFile($file) {
502
-		if (pathinfo($file, PATHINFO_EXTENSION) === 'part') {
503
-			return true;
504
-		}
505
-		if (strpos($file, '.part/') !== false) {
506
-			return true;
507
-		}
508
-
509
-		return false;
510
-	}
511
-
512
-	/**
513
-	 * walk over any folders that are not fully scanned yet and scan them
514
-	 */
515
-	public function backgroundScan() {
516
-		if ($this->storage->instanceOfStorage(Jail::class)) {
517
-			// for jail storage wrappers (shares, groupfolders) we run the background scan on the source storage
518
-			// this is mainly done because the jail wrapper doesn't implement `getIncomplete` (because it would be inefficient).
519
-			//
520
-			// Running the scan on the source storage might scan more than "needed", but the unscanned files outside the jail will
521
-			// have to be scanned at some point anyway.
522
-			$unJailedScanner = $this->storage->getUnjailedStorage()->getScanner();
523
-			$unJailedScanner->backgroundScan();
524
-		} else {
525
-			if (!$this->cache->inCache('')) {
526
-				// if the storage isn't in the cache yet, just scan the root completely
527
-				$this->runBackgroundScanJob(function () {
528
-					$this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
529
-				}, '');
530
-			} else {
531
-				$lastPath = null;
532
-				// find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
533
-				while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
534
-					$this->runBackgroundScanJob(function () use ($path) {
535
-						$this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
536
-					}, $path);
537
-					// FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
538
-					// to make this possible
539
-					$lastPath = $path;
540
-				}
541
-			}
542
-		}
543
-	}
544
-
545
-	private function runBackgroundScanJob(callable $callback, $path) {
546
-		try {
547
-			$callback();
548
-			\OC_Hook::emit('Scanner', 'correctFolderSize', ['path' => $path]);
549
-			if ($this->cacheActive && $this->cache instanceof Cache) {
550
-				$this->cache->correctFolderSize($path, null, true);
551
-			}
552
-		} catch (\OCP\Files\StorageInvalidException $e) {
553
-			// skip unavailable storages
554
-		} catch (\OCP\Files\StorageNotAvailableException $e) {
555
-			// skip unavailable storages
556
-		} catch (\OCP\Files\ForbiddenException $e) {
557
-			// skip forbidden storages
558
-		} catch (\OCP\Lock\LockedException $e) {
559
-			// skip unavailable storages
560
-		}
561
-	}
562
-
563
-	/**
564
-	 * Set whether the cache is affected by scan operations
565
-	 *
566
-	 * @param boolean $active The active state of the cache
567
-	 */
568
-	public function setCacheActive($active) {
569
-		$this->cacheActive = $active;
570
-	}
61
+    /**
62
+     * @var \OC\Files\Storage\Storage $storage
63
+     */
64
+    protected $storage;
65
+
66
+    /**
67
+     * @var string $storageId
68
+     */
69
+    protected $storageId;
70
+
71
+    /**
72
+     * @var \OC\Files\Cache\Cache $cache
73
+     */
74
+    protected $cache;
75
+
76
+    /**
77
+     * @var boolean $cacheActive If true, perform cache operations, if false, do not affect cache
78
+     */
79
+    protected $cacheActive;
80
+
81
+    /**
82
+     * @var bool $useTransactions whether to use transactions
83
+     */
84
+    protected $useTransactions = true;
85
+
86
+    /**
87
+     * @var \OCP\Lock\ILockingProvider
88
+     */
89
+    protected $lockingProvider;
90
+
91
+    public function __construct(\OC\Files\Storage\Storage $storage) {
92
+        $this->storage = $storage;
93
+        $this->storageId = $this->storage->getId();
94
+        $this->cache = $storage->getCache();
95
+        $this->cacheActive = !\OC::$server->getConfig()->getSystemValue('filesystem_cache_readonly', false);
96
+        $this->lockingProvider = \OC::$server->getLockingProvider();
97
+    }
98
+
99
+    /**
100
+     * Whether to wrap the scanning of a folder in a database transaction
101
+     * On default transactions are used
102
+     *
103
+     * @param bool $useTransactions
104
+     */
105
+    public function setUseTransactions($useTransactions) {
106
+        $this->useTransactions = $useTransactions;
107
+    }
108
+
109
+    /**
110
+     * get all the metadata of a file or folder
111
+     * *
112
+     *
113
+     * @param string $path
114
+     * @return array|null an array of metadata of the file
115
+     */
116
+    protected function getData($path) {
117
+        $data = $this->storage->getMetaData($path);
118
+        if (is_null($data)) {
119
+            \OC::$server->get(LoggerInterface::class)->debug("!!! Path '$path' is not accessible or present !!!", ['app' => 'core']);
120
+        }
121
+        return $data;
122
+    }
123
+
124
+    /**
125
+     * scan a single file and store it in the cache
126
+     *
127
+     * @param string $file
128
+     * @param int $reuseExisting
129
+     * @param int $parentId
130
+     * @param array|null|false $cacheData existing data in the cache for the file to be scanned
131
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
132
+     * @param null $data the metadata for the file, as returned by the storage
133
+     * @return array|null an array of metadata of the scanned file
134
+     * @throws \OCP\Lock\LockedException
135
+     */
136
+    public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
137
+        if ($file !== '') {
138
+            try {
139
+                $this->storage->verifyPath(dirname($file), basename($file));
140
+            } catch (\Exception $e) {
141
+                return null;
142
+            }
143
+        }
144
+        // only proceed if $file is not a partial file, blacklist is handled by the storage
145
+        if (!self::isPartialFile($file)) {
146
+            // acquire a lock
147
+            if ($lock) {
148
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
149
+                    $this->storage->acquireLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
150
+                }
151
+            }
152
+
153
+            try {
154
+                $data = $data ?? $this->getData($file);
155
+            } catch (ForbiddenException $e) {
156
+                if ($lock) {
157
+                    if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
158
+                        $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
159
+                    }
160
+                }
161
+
162
+                return null;
163
+            }
164
+
165
+            try {
166
+                if ($data) {
167
+                    // pre-emit only if it was a file. By that we avoid counting/treating folders as files
168
+                    if ($data['mimetype'] !== 'httpd/unix-directory') {
169
+                        $this->emit('\OC\Files\Cache\Scanner', 'scanFile', [$file, $this->storageId]);
170
+                        \OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', ['path' => $file, 'storage' => $this->storageId]);
171
+                    }
172
+
173
+                    $parent = dirname($file);
174
+                    if ($parent === '.' || $parent === '/') {
175
+                        $parent = '';
176
+                    }
177
+                    if ($parentId === -1) {
178
+                        $parentId = $this->cache->getParentId($file);
179
+                    }
180
+
181
+                    // scan the parent if it's not in the cache (id -1) and the current file is not the root folder
182
+                    if ($file && $parentId === -1) {
183
+                        $parentData = $this->scanFile($parent);
184
+                        if (!$parentData) {
185
+                            return null;
186
+                        }
187
+                        $parentId = $parentData['fileid'];
188
+                    }
189
+                    if ($parent) {
190
+                        $data['parent'] = $parentId;
191
+                    }
192
+                    if (is_null($cacheData)) {
193
+                        /** @var CacheEntry $cacheData */
194
+                        $cacheData = $this->cache->get($file);
195
+                    }
196
+                    if ($cacheData && $reuseExisting && isset($cacheData['fileid'])) {
197
+                        // prevent empty etag
198
+                        $etag = empty($cacheData['etag']) ? $data['etag'] : $cacheData['etag'];
199
+                        $fileId = $cacheData['fileid'];
200
+                        $data['fileid'] = $fileId;
201
+                        // only reuse data if the file hasn't explicitly changed
202
+                        if (isset($data['storage_mtime']) && isset($cacheData['storage_mtime']) && $data['storage_mtime'] === $cacheData['storage_mtime']) {
203
+                            $data['mtime'] = $cacheData['mtime'];
204
+                            if (($reuseExisting & self::REUSE_SIZE) && ($data['size'] === -1)) {
205
+                                $data['size'] = $cacheData['size'];
206
+                            }
207
+                            if ($reuseExisting & self::REUSE_ETAG && !$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
208
+                                $data['etag'] = $etag;
209
+                            }
210
+                        }
211
+                        // Only update metadata that has changed
212
+                        $newData = array_diff_assoc($data, $cacheData->getData());
213
+                    } else {
214
+                        $newData = $data;
215
+                        $fileId = -1;
216
+                    }
217
+                    if (!empty($newData)) {
218
+                        // Reset the checksum if the data has changed
219
+                        $newData['checksum'] = '';
220
+                        $newData['parent'] = $parentId;
221
+                        $data['fileid'] = $this->addToCache($file, $newData, $fileId);
222
+                    }
223
+
224
+                    $data['oldSize'] = ($cacheData && isset($cacheData['size'])) ? $cacheData['size'] : 0;
225
+
226
+                    if ($cacheData && isset($cacheData['encrypted'])) {
227
+                        $data['encrypted'] = $cacheData['encrypted'];
228
+                    }
229
+
230
+                    // post-emit only if it was a file. By that we avoid counting/treating folders as files
231
+                    if ($data['mimetype'] !== 'httpd/unix-directory') {
232
+                        $this->emit('\OC\Files\Cache\Scanner', 'postScanFile', [$file, $this->storageId]);
233
+                        \OC_Hook::emit('\OC\Files\Cache\Scanner', 'post_scan_file', ['path' => $file, 'storage' => $this->storageId]);
234
+                    }
235
+                } else {
236
+                    $this->removeFromCache($file);
237
+                }
238
+            } catch (\Exception $e) {
239
+                if ($lock) {
240
+                    if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
241
+                        $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
242
+                    }
243
+                }
244
+                throw $e;
245
+            }
246
+
247
+            // release the acquired lock
248
+            if ($lock) {
249
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
250
+                    $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
251
+                }
252
+            }
253
+
254
+            if ($data && !isset($data['encrypted'])) {
255
+                $data['encrypted'] = false;
256
+            }
257
+            return $data;
258
+        }
259
+
260
+        return null;
261
+    }
262
+
263
+    protected function removeFromCache($path) {
264
+        \OC_Hook::emit('Scanner', 'removeFromCache', ['file' => $path]);
265
+        $this->emit('\OC\Files\Cache\Scanner', 'removeFromCache', [$path]);
266
+        if ($this->cacheActive) {
267
+            $this->cache->remove($path);
268
+        }
269
+    }
270
+
271
+    /**
272
+     * @param string $path
273
+     * @param array $data
274
+     * @param int $fileId
275
+     * @return int the id of the added file
276
+     */
277
+    protected function addToCache($path, $data, $fileId = -1) {
278
+        if (isset($data['scan_permissions'])) {
279
+            $data['permissions'] = $data['scan_permissions'];
280
+        }
281
+        \OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
282
+        $this->emit('\OC\Files\Cache\Scanner', 'addToCache', [$path, $this->storageId, $data]);
283
+        if ($this->cacheActive) {
284
+            if ($fileId !== -1) {
285
+                $this->cache->update($fileId, $data);
286
+                return $fileId;
287
+            } else {
288
+                return $this->cache->insert($path, $data);
289
+            }
290
+        } else {
291
+            return -1;
292
+        }
293
+    }
294
+
295
+    /**
296
+     * @param string $path
297
+     * @param array $data
298
+     * @param int $fileId
299
+     */
300
+    protected function updateCache($path, $data, $fileId = -1) {
301
+        \OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
302
+        $this->emit('\OC\Files\Cache\Scanner', 'updateCache', [$path, $this->storageId, $data]);
303
+        if ($this->cacheActive) {
304
+            if ($fileId !== -1) {
305
+                $this->cache->update($fileId, $data);
306
+            } else {
307
+                $this->cache->put($path, $data);
308
+            }
309
+        }
310
+    }
311
+
312
+    /**
313
+     * scan a folder and all it's children
314
+     *
315
+     * @param string $path
316
+     * @param bool $recursive
317
+     * @param int $reuse
318
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
319
+     * @return array|null an array of the meta data of the scanned file or folder
320
+     */
321
+    public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
322
+        if ($reuse === -1) {
323
+            $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
324
+        }
325
+        if ($lock) {
326
+            if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
327
+                $this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
328
+                $this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
329
+            }
330
+        }
331
+        try {
332
+            try {
333
+                $data = $this->scanFile($path, $reuse, -1, null, $lock);
334
+                if ($data && $data['mimetype'] === 'httpd/unix-directory') {
335
+                    $size = $this->scanChildren($path, $recursive, $reuse, $data['fileid'], $lock, $data);
336
+                    $data['size'] = $size;
337
+                }
338
+            } catch (NotFoundException $e) {
339
+                $this->removeFromCache($path);
340
+                return null;
341
+            }
342
+        } finally {
343
+            if ($lock) {
344
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
345
+                    $this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
346
+                    $this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
347
+                }
348
+            }
349
+        }
350
+        return $data;
351
+    }
352
+
353
+    /**
354
+     * Get the children currently in the cache
355
+     *
356
+     * @param int $folderId
357
+     * @return array[]
358
+     */
359
+    protected function getExistingChildren($folderId) {
360
+        $existingChildren = [];
361
+        $children = $this->cache->getFolderContentsById($folderId);
362
+        foreach ($children as $child) {
363
+            $existingChildren[$child['name']] = $child;
364
+        }
365
+        return $existingChildren;
366
+    }
367
+
368
+    /**
369
+     * scan all the files and folders in a folder
370
+     *
371
+     * @param string $path
372
+     * @param bool $recursive
373
+     * @param int $reuse
374
+     * @param int $folderId id for the folder to be scanned
375
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
376
+     * @param array $data the data of the folder before (re)scanning the children
377
+     * @return int the size of the scanned folder or -1 if the size is unknown at this stage
378
+     */
379
+    protected function scanChildren($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $folderId = null, $lock = true, array $data = []) {
380
+        if ($reuse === -1) {
381
+            $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
382
+        }
383
+        $this->emit('\OC\Files\Cache\Scanner', 'scanFolder', [$path, $this->storageId]);
384
+        $size = 0;
385
+        if (!is_null($folderId)) {
386
+            $folderId = $this->cache->getId($path);
387
+        }
388
+        $childQueue = $this->handleChildren($path, $recursive, $reuse, $folderId, $lock, $size);
389
+
390
+        foreach ($childQueue as $child => $childId) {
391
+            $childSize = $this->scanChildren($child, $recursive, $reuse, $childId, $lock);
392
+            if ($childSize === -1) {
393
+                $size = -1;
394
+            } elseif ($size !== -1) {
395
+                $size += $childSize;
396
+            }
397
+        }
398
+        $oldSize = $data['size'] ?? null;
399
+        if ($this->cacheActive && $oldSize !== $size) {
400
+            $this->cache->update($folderId, ['size' => $size]);
401
+        }
402
+        $this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', [$path, $this->storageId]);
403
+        return $size;
404
+    }
405
+
406
+    private function handleChildren($path, $recursive, $reuse, $folderId, $lock, &$size) {
407
+        // we put this in it's own function so it cleans up the memory before we start recursing
408
+        $existingChildren = $this->getExistingChildren($folderId);
409
+        $newChildren = iterator_to_array($this->storage->getDirectoryContent($path));
410
+
411
+        if (count($existingChildren) === 0 && count($newChildren) === 0) {
412
+            // no need to do a transaction
413
+            return [];
414
+        }
415
+
416
+        if ($this->useTransactions) {
417
+            \OC::$server->getDatabaseConnection()->beginTransaction();
418
+        }
419
+
420
+        $exceptionOccurred = false;
421
+        $childQueue = [];
422
+        $newChildNames = [];
423
+        foreach ($newChildren as $fileMeta) {
424
+            $permissions = isset($fileMeta['scan_permissions']) ? $fileMeta['scan_permissions'] : $fileMeta['permissions'];
425
+            if ($permissions === 0) {
426
+                continue;
427
+            }
428
+            $originalFile = $fileMeta['name'];
429
+            $file = trim(\OC\Files\Filesystem::normalizePath($originalFile), '/');
430
+            if (trim($originalFile, '/') !== $file) {
431
+                // encoding mismatch, might require compatibility wrapper
432
+                \OC::$server->get(LoggerInterface::class)->debug('Scanner: Skipping non-normalized file name "'. $originalFile . '" in path "' . $path . '".', ['app' => 'core']);
433
+                $this->emit('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', [$path ? $path . '/' . $originalFile : $originalFile]);
434
+                // skip this entry
435
+                continue;
436
+            }
437
+
438
+            $newChildNames[] = $file;
439
+            $child = $path ? $path . '/' . $file : $file;
440
+            try {
441
+                $existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : false;
442
+                $data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock, $fileMeta);
443
+                if ($data) {
444
+                    if ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE) {
445
+                        $childQueue[$child] = $data['fileid'];
446
+                    } elseif ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE_INCOMPLETE && $data['size'] === -1) {
447
+                        // only recurse into folders which aren't fully scanned
448
+                        $childQueue[$child] = $data['fileid'];
449
+                    } elseif ($data['size'] === -1) {
450
+                        $size = -1;
451
+                    } elseif ($size !== -1) {
452
+                        $size += $data['size'];
453
+                    }
454
+                }
455
+            } catch (Exception $ex) {
456
+                // might happen if inserting duplicate while a scanning
457
+                // process is running in parallel
458
+                // log and ignore
459
+                if ($this->useTransactions) {
460
+                    \OC::$server->getDatabaseConnection()->rollback();
461
+                    \OC::$server->getDatabaseConnection()->beginTransaction();
462
+                }
463
+                \OC::$server->get(LoggerInterface::class)->debug('Exception while scanning file "' . $child . '"', [
464
+                    'app' => 'core',
465
+                    'exception' => $ex,
466
+                ]);
467
+                $exceptionOccurred = true;
468
+            } catch (\OCP\Lock\LockedException $e) {
469
+                if ($this->useTransactions) {
470
+                    \OC::$server->getDatabaseConnection()->rollback();
471
+                }
472
+                throw $e;
473
+            }
474
+        }
475
+        $removedChildren = \array_diff(array_keys($existingChildren), $newChildNames);
476
+        foreach ($removedChildren as $childName) {
477
+            $child = $path ? $path . '/' . $childName : $childName;
478
+            $this->removeFromCache($child);
479
+        }
480
+        if ($this->useTransactions) {
481
+            \OC::$server->getDatabaseConnection()->commit();
482
+        }
483
+        if ($exceptionOccurred) {
484
+            // It might happen that the parallel scan process has already
485
+            // inserted mimetypes but those weren't available yet inside the transaction
486
+            // To make sure to have the updated mime types in such cases,
487
+            // we reload them here
488
+            \OC::$server->getMimeTypeLoader()->reset();
489
+        }
490
+        return $childQueue;
491
+    }
492
+
493
+    /**
494
+     * check if the file should be ignored when scanning
495
+     * NOTE: files with a '.part' extension are ignored as well!
496
+     *       prevents unfinished put requests to be scanned
497
+     *
498
+     * @param string $file
499
+     * @return boolean
500
+     */
501
+    public static function isPartialFile($file) {
502
+        if (pathinfo($file, PATHINFO_EXTENSION) === 'part') {
503
+            return true;
504
+        }
505
+        if (strpos($file, '.part/') !== false) {
506
+            return true;
507
+        }
508
+
509
+        return false;
510
+    }
511
+
512
+    /**
513
+     * walk over any folders that are not fully scanned yet and scan them
514
+     */
515
+    public function backgroundScan() {
516
+        if ($this->storage->instanceOfStorage(Jail::class)) {
517
+            // for jail storage wrappers (shares, groupfolders) we run the background scan on the source storage
518
+            // this is mainly done because the jail wrapper doesn't implement `getIncomplete` (because it would be inefficient).
519
+            //
520
+            // Running the scan on the source storage might scan more than "needed", but the unscanned files outside the jail will
521
+            // have to be scanned at some point anyway.
522
+            $unJailedScanner = $this->storage->getUnjailedStorage()->getScanner();
523
+            $unJailedScanner->backgroundScan();
524
+        } else {
525
+            if (!$this->cache->inCache('')) {
526
+                // if the storage isn't in the cache yet, just scan the root completely
527
+                $this->runBackgroundScanJob(function () {
528
+                    $this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
529
+                }, '');
530
+            } else {
531
+                $lastPath = null;
532
+                // find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
533
+                while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
534
+                    $this->runBackgroundScanJob(function () use ($path) {
535
+                        $this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
536
+                    }, $path);
537
+                    // FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
538
+                    // to make this possible
539
+                    $lastPath = $path;
540
+                }
541
+            }
542
+        }
543
+    }
544
+
545
+    private function runBackgroundScanJob(callable $callback, $path) {
546
+        try {
547
+            $callback();
548
+            \OC_Hook::emit('Scanner', 'correctFolderSize', ['path' => $path]);
549
+            if ($this->cacheActive && $this->cache instanceof Cache) {
550
+                $this->cache->correctFolderSize($path, null, true);
551
+            }
552
+        } catch (\OCP\Files\StorageInvalidException $e) {
553
+            // skip unavailable storages
554
+        } catch (\OCP\Files\StorageNotAvailableException $e) {
555
+            // skip unavailable storages
556
+        } catch (\OCP\Files\ForbiddenException $e) {
557
+            // skip forbidden storages
558
+        } catch (\OCP\Lock\LockedException $e) {
559
+            // skip unavailable storages
560
+        }
561
+    }
562
+
563
+    /**
564
+     * Set whether the cache is affected by scan operations
565
+     *
566
+     * @param boolean $active The active state of the cache
567
+     */
568
+    public function setCacheActive($active) {
569
+        $this->cacheActive = $active;
570
+    }
571 571
 }
Please login to merge, or discard this patch.
apps/files_external/lib/Lib/Storage/SMB.php 2 patches
Indentation   +687 added lines, -687 removed lines patch added patch discarded remove patch
@@ -69,691 +69,691 @@
 block discarded – undo
69 69
 use OCP\ILogger;
70 70
 
71 71
 class SMB extends Common implements INotifyStorage {
72
-	/**
73
-	 * @var \Icewind\SMB\IServer
74
-	 */
75
-	protected $server;
76
-
77
-	/**
78
-	 * @var \Icewind\SMB\IShare
79
-	 */
80
-	protected $share;
81
-
82
-	/**
83
-	 * @var string
84
-	 */
85
-	protected $root;
86
-
87
-	/** @var CappedMemoryCache<IFileInfo> */
88
-	protected CappedMemoryCache $statCache;
89
-
90
-	/** @var ILogger */
91
-	protected $logger;
92
-
93
-	/** @var bool */
94
-	protected $showHidden;
95
-
96
-	/** @var bool */
97
-	protected $checkAcl;
98
-
99
-	public function __construct($params) {
100
-		if (!isset($params['host'])) {
101
-			throw new \Exception('Invalid configuration, no host provided');
102
-		}
103
-
104
-		if (isset($params['auth'])) {
105
-			$auth = $params['auth'];
106
-		} elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
107
-			[$workgroup, $user] = $this->splitUser($params['user']);
108
-			$auth = new BasicAuth($user, $workgroup, $params['password']);
109
-		} else {
110
-			throw new \Exception('Invalid configuration, no credentials provided');
111
-		}
112
-
113
-		if (isset($params['logger'])) {
114
-			$this->logger = $params['logger'];
115
-		} else {
116
-			$this->logger = \OC::$server->getLogger();
117
-		}
118
-
119
-		$options = new Options();
120
-		if (isset($params['timeout'])) {
121
-			$timeout = (int)$params['timeout'];
122
-			if ($timeout > 0) {
123
-				$options->setTimeout($timeout);
124
-			}
125
-		}
126
-		$serverFactory = new ServerFactory($options);
127
-		$this->server = $serverFactory->createServer($params['host'], $auth);
128
-		$this->share = $this->server->getShare(trim($params['share'], '/'));
129
-
130
-		$this->root = $params['root'] ?? '/';
131
-		$this->root = '/' . ltrim($this->root, '/');
132
-		$this->root = rtrim($this->root, '/') . '/';
133
-
134
-		$this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
135
-		$this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
136
-
137
-		$this->statCache = new CappedMemoryCache();
138
-		parent::__construct($params);
139
-	}
140
-
141
-	private function splitUser($user) {
142
-		if (strpos($user, '/')) {
143
-			return explode('/', $user, 2);
144
-		} elseif (strpos($user, '\\')) {
145
-			return explode('\\', $user);
146
-		} else {
147
-			return [null, $user];
148
-		}
149
-	}
150
-
151
-	/**
152
-	 * @return string
153
-	 */
154
-	public function getId() {
155
-		// FIXME: double slash to keep compatible with the old storage ids,
156
-		// failure to do so will lead to creation of a new storage id and
157
-		// loss of shares from the storage
158
-		return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
159
-	}
160
-
161
-	/**
162
-	 * @param string $path
163
-	 * @return string
164
-	 */
165
-	protected function buildPath($path) {
166
-		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
167
-	}
168
-
169
-	protected function relativePath($fullPath) {
170
-		if ($fullPath === $this->root) {
171
-			return '';
172
-		} elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
173
-			return substr($fullPath, strlen($this->root));
174
-		} else {
175
-			return null;
176
-		}
177
-	}
178
-
179
-	/**
180
-	 * @param string $path
181
-	 * @return IFileInfo
182
-	 * @throws StorageAuthException
183
-	 */
184
-	protected function getFileInfo($path) {
185
-		try {
186
-			$path = $this->buildPath($path);
187
-			$cached = $this->statCache[$path] ?? null;
188
-			if ($cached instanceof IFileInfo) {
189
-				return $cached;
190
-			} else {
191
-				$stat = $this->share->stat($path);
192
-				$this->statCache[$path] = $stat;
193
-				return $stat;
194
-			}
195
-		} catch (ConnectException $e) {
196
-			$this->throwUnavailable($e);
197
-		} catch (NotFoundException $e) {
198
-			throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
199
-		} catch (ForbiddenException $e) {
200
-			// with php-smbclient, this exception is thrown when the provided password is invalid.
201
-			// Possible is also ForbiddenException with a different error code, so we check it.
202
-			if ($e->getCode() === 1) {
203
-				$this->throwUnavailable($e);
204
-			}
205
-			throw new \OCP\Files\ForbiddenException($e->getMessage(), false, $e);
206
-		}
207
-	}
208
-
209
-	/**
210
-	 * @param \Exception $e
211
-	 * @return never
212
-	 * @throws StorageAuthException
213
-	 */
214
-	protected function throwUnavailable(\Exception $e) {
215
-		$this->logger->logException($e, ['message' => 'Error while getting file info']);
216
-		throw new StorageAuthException($e->getMessage(), $e);
217
-	}
218
-
219
-	/**
220
-	 * get the acl from fileinfo that is relevant for the configured user
221
-	 *
222
-	 * @param IFileInfo $file
223
-	 * @return ACL|null
224
-	 */
225
-	private function getACL(IFileInfo $file): ?ACL {
226
-		$acls = $file->getAcls();
227
-		foreach ($acls as $user => $acl) {
228
-			[, $user] = $this->splitUser($user); // strip domain
229
-			if ($user === $this->server->getAuth()->getUsername()) {
230
-				return $acl;
231
-			}
232
-		}
233
-
234
-		return null;
235
-	}
236
-
237
-	/**
238
-	 * @param string $path
239
-	 * @return \Generator<IFileInfo>
240
-	 * @throws StorageNotAvailableException
241
-	 */
242
-	protected function getFolderContents($path): iterable {
243
-		try {
244
-			$path = ltrim($this->buildPath($path), '/');
245
-			try {
246
-				$files = $this->share->dir($path);
247
-			} catch (ForbiddenException $e) {
248
-				$this->logger->critical($e->getMessage(), ['exception' => $e]);
249
-				throw new NotPermittedException();
250
-			} catch (InvalidTypeException $e) {
251
-				return;
252
-			}
253
-			foreach ($files as $file) {
254
-				$this->statCache[$path . '/' . $file->getName()] = $file;
255
-			}
256
-
257
-			foreach ($files as $file) {
258
-				try {
259
-					// the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
260
-					// so we trigger the below exceptions where applicable
261
-					$hide = $file->isHidden() && !$this->showHidden;
262
-
263
-					if ($this->checkAcl && $acl = $this->getACL($file)) {
264
-						// if there is no explicit deny, we assume it's allowed
265
-						// this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
266
-						// additionally, it's better to have false negatives here then false positives
267
-						if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
268
-							$this->logger->debug('Hiding non readable entry ' . $file->getName());
269
-							continue;
270
-						}
271
-					}
272
-
273
-					if ($hide) {
274
-						$this->logger->debug('hiding hidden file ' . $file->getName());
275
-					}
276
-					if (!$hide) {
277
-						yield $file;
278
-					}
279
-				} catch (ForbiddenException $e) {
280
-					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);
281
-				} catch (NotFoundException $e) {
282
-					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);
283
-				}
284
-			}
285
-		} catch (ConnectException $e) {
286
-			$this->logger->logException($e, ['message' => 'Error while getting folder content']);
287
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
288
-		} catch (NotFoundException $e) {
289
-			throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
290
-		}
291
-	}
292
-
293
-	/**
294
-	 * @param IFileInfo $info
295
-	 * @return array
296
-	 */
297
-	protected function formatInfo($info) {
298
-		$result = [
299
-			'size' => $info->getSize(),
300
-			'mtime' => $info->getMTime(),
301
-		];
302
-		if ($info->isDirectory()) {
303
-			$result['type'] = 'dir';
304
-		} else {
305
-			$result['type'] = 'file';
306
-		}
307
-		return $result;
308
-	}
309
-
310
-	/**
311
-	 * Rename the files. If the source or the target is the root, the rename won't happen.
312
-	 *
313
-	 * @param string $source the old name of the path
314
-	 * @param string $target the new name of the path
315
-	 * @return bool true if the rename is successful, false otherwise
316
-	 */
317
-	public function rename($source, $target, $retry = true): bool {
318
-		if ($this->isRootDir($source) || $this->isRootDir($target)) {
319
-			return false;
320
-		}
321
-
322
-		$absoluteSource = $this->buildPath($source);
323
-		$absoluteTarget = $this->buildPath($target);
324
-		try {
325
-			$result = $this->share->rename($absoluteSource, $absoluteTarget);
326
-		} catch (AlreadyExistsException $e) {
327
-			if ($retry) {
328
-				$this->remove($target);
329
-				$result = $this->share->rename($absoluteSource, $absoluteTarget);
330
-			} else {
331
-				$this->logger->logException($e, ['level' => ILogger::WARN]);
332
-				return false;
333
-			}
334
-		} catch (InvalidArgumentException $e) {
335
-			if ($retry) {
336
-				$this->remove($target);
337
-				$result = $this->share->rename($absoluteSource, $absoluteTarget);
338
-			} else {
339
-				$this->logger->logException($e, ['level' => ILogger::WARN]);
340
-				return false;
341
-			}
342
-		} catch (\Exception $e) {
343
-			$this->logger->logException($e, ['level' => ILogger::WARN]);
344
-			return false;
345
-		}
346
-		unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
347
-		return $result;
348
-	}
349
-
350
-	public function stat($path, $retry = true) {
351
-		try {
352
-			$result = $this->formatInfo($this->getFileInfo($path));
353
-		} catch (ForbiddenException $e) {
354
-			return false;
355
-		} catch (\OCP\Files\NotFoundException $e) {
356
-			return false;
357
-		} catch (TimedOutException $e) {
358
-			if ($retry) {
359
-				return $this->stat($path, false);
360
-			} else {
361
-				throw $e;
362
-			}
363
-		}
364
-		if ($this->remoteIsShare() && $this->isRootDir($path)) {
365
-			$result['mtime'] = $this->shareMTime();
366
-		}
367
-		return $result;
368
-	}
369
-
370
-	/**
371
-	 * get the best guess for the modification time of the share
372
-	 *
373
-	 * @return int
374
-	 */
375
-	private function shareMTime() {
376
-		$highestMTime = 0;
377
-		$files = $this->share->dir($this->root);
378
-		foreach ($files as $fileInfo) {
379
-			try {
380
-				if ($fileInfo->getMTime() > $highestMTime) {
381
-					$highestMTime = $fileInfo->getMTime();
382
-				}
383
-			} catch (NotFoundException $e) {
384
-				// Ignore this, can happen on unavailable DFS shares
385
-			} catch (ForbiddenException $e) {
386
-				// Ignore this too - it's a symlink
387
-			}
388
-		}
389
-		return $highestMTime;
390
-	}
391
-
392
-	/**
393
-	 * Check if the path is our root dir (not the smb one)
394
-	 *
395
-	 * @param string $path the path
396
-	 * @return bool
397
-	 */
398
-	private function isRootDir($path) {
399
-		return $path === '' || $path === '/' || $path === '.';
400
-	}
401
-
402
-	/**
403
-	 * Check if our root points to a smb share
404
-	 *
405
-	 * @return bool true if our root points to a share false otherwise
406
-	 */
407
-	private function remoteIsShare() {
408
-		return $this->share->getName() && (!$this->root || $this->root === '/');
409
-	}
410
-
411
-	/**
412
-	 * @param string $path
413
-	 * @return bool
414
-	 */
415
-	public function unlink($path) {
416
-		if ($this->isRootDir($path)) {
417
-			return false;
418
-		}
419
-
420
-		try {
421
-			if ($this->is_dir($path)) {
422
-				return $this->rmdir($path);
423
-			} else {
424
-				$path = $this->buildPath($path);
425
-				unset($this->statCache[$path]);
426
-				$this->share->del($path);
427
-				return true;
428
-			}
429
-		} catch (NotFoundException $e) {
430
-			return false;
431
-		} catch (ForbiddenException $e) {
432
-			return false;
433
-		} catch (ConnectException $e) {
434
-			$this->logger->logException($e, ['message' => 'Error while deleting file']);
435
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
436
-		}
437
-	}
438
-
439
-	/**
440
-	 * check if a file or folder has been updated since $time
441
-	 *
442
-	 * @param string $path
443
-	 * @param int $time
444
-	 * @return bool
445
-	 */
446
-	public function hasUpdated($path, $time) {
447
-		if (!$path and $this->root === '/') {
448
-			// mtime doesn't work for shares, but giving the nature of the backend,
449
-			// doing a full update is still just fast enough
450
-			return true;
451
-		} else {
452
-			$actualTime = $this->filemtime($path);
453
-			return $actualTime > $time;
454
-		}
455
-	}
456
-
457
-	/**
458
-	 * @param string $path
459
-	 * @param string $mode
460
-	 * @return resource|bool
461
-	 */
462
-	public function fopen($path, $mode) {
463
-		$fullPath = $this->buildPath($path);
464
-		try {
465
-			switch ($mode) {
466
-				case 'r':
467
-				case 'rb':
468
-					if (!$this->file_exists($path)) {
469
-						return false;
470
-					}
471
-					return $this->share->read($fullPath);
472
-				case 'w':
473
-				case 'wb':
474
-					$source = $this->share->write($fullPath);
475
-					return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
476
-						unset($this->statCache[$fullPath]);
477
-					});
478
-				case 'a':
479
-				case 'ab':
480
-				case 'r+':
481
-				case 'w+':
482
-				case 'wb+':
483
-				case 'a+':
484
-				case 'x':
485
-				case 'x+':
486
-				case 'c':
487
-				case 'c+':
488
-					//emulate these
489
-					if (strrpos($path, '.') !== false) {
490
-						$ext = substr($path, strrpos($path, '.'));
491
-					} else {
492
-						$ext = '';
493
-					}
494
-					if ($this->file_exists($path)) {
495
-						if (!$this->isUpdatable($path)) {
496
-							return false;
497
-						}
498
-						$tmpFile = $this->getCachedFile($path);
499
-					} else {
500
-						if (!$this->isCreatable(dirname($path))) {
501
-							return false;
502
-						}
503
-						$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
504
-					}
505
-					$source = fopen($tmpFile, $mode);
506
-					$share = $this->share;
507
-					return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
508
-						unset($this->statCache[$fullPath]);
509
-						$share->put($tmpFile, $fullPath);
510
-						unlink($tmpFile);
511
-					});
512
-			}
513
-			return false;
514
-		} catch (NotFoundException $e) {
515
-			return false;
516
-		} catch (ForbiddenException $e) {
517
-			return false;
518
-		} catch (OutOfSpaceException $e) {
519
-			throw new EntityTooLargeException("not enough available space to create file", 0, $e);
520
-		} catch (ConnectException $e) {
521
-			$this->logger->logException($e, ['message' => 'Error while opening file']);
522
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
523
-		}
524
-	}
525
-
526
-	public function rmdir($path) {
527
-		if ($this->isRootDir($path)) {
528
-			return false;
529
-		}
530
-
531
-		try {
532
-			$this->statCache = new CappedMemoryCache();
533
-			$content = $this->share->dir($this->buildPath($path));
534
-			foreach ($content as $file) {
535
-				if ($file->isDirectory()) {
536
-					$this->rmdir($path . '/' . $file->getName());
537
-				} else {
538
-					$this->share->del($file->getPath());
539
-				}
540
-			}
541
-			$this->share->rmdir($this->buildPath($path));
542
-			return true;
543
-		} catch (NotFoundException $e) {
544
-			return false;
545
-		} catch (ForbiddenException $e) {
546
-			return false;
547
-		} catch (ConnectException $e) {
548
-			$this->logger->logException($e, ['message' => 'Error while removing folder']);
549
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
550
-		}
551
-	}
552
-
553
-	public function touch($path, $mtime = null) {
554
-		try {
555
-			if (!$this->file_exists($path)) {
556
-				$fh = $this->share->write($this->buildPath($path));
557
-				fclose($fh);
558
-				return true;
559
-			}
560
-			return false;
561
-		} catch (OutOfSpaceException $e) {
562
-			throw new EntityTooLargeException("not enough available space to create file", 0, $e);
563
-		} catch (ConnectException $e) {
564
-			$this->logger->logException($e, ['message' => 'Error while creating file']);
565
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
566
-		}
567
-	}
568
-
569
-	public function getMetaData($path) {
570
-		try {
571
-			$fileInfo = $this->getFileInfo($path);
572
-		} catch (\OCP\Files\NotFoundException $e) {
573
-			return null;
574
-		} catch (ForbiddenException $e) {
575
-			return null;
576
-		}
577
-		if (!$fileInfo) {
578
-			return null;
579
-		}
580
-
581
-		return $this->getMetaDataFromFileInfo($fileInfo);
582
-	}
583
-
584
-	private function getMetaDataFromFileInfo(IFileInfo $fileInfo) {
585
-		$permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
586
-
587
-		if (
588
-			!$fileInfo->isReadOnly() || $fileInfo->isDirectory()
589
-		) {
590
-			$permissions += Constants::PERMISSION_DELETE;
591
-			$permissions += Constants::PERMISSION_UPDATE;
592
-			if ($fileInfo->isDirectory()) {
593
-				$permissions += Constants::PERMISSION_CREATE;
594
-			}
595
-		}
596
-
597
-		$data = [];
598
-		if ($fileInfo->isDirectory()) {
599
-			$data['mimetype'] = 'httpd/unix-directory';
600
-		} else {
601
-			$data['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath());
602
-		}
603
-		$data['mtime'] = $fileInfo->getMTime();
604
-		if ($fileInfo->isDirectory()) {
605
-			$data['size'] = -1; //unknown
606
-		} else {
607
-			$data['size'] = $fileInfo->getSize();
608
-		}
609
-		$data['etag'] = $this->getETag($fileInfo->getPath());
610
-		$data['storage_mtime'] = $data['mtime'];
611
-		$data['permissions'] = $permissions;
612
-		$data['name'] = $fileInfo->getName();
613
-
614
-		return $data;
615
-	}
616
-
617
-	public function opendir($path) {
618
-		try {
619
-			$files = $this->getFolderContents($path);
620
-		} catch (NotFoundException $e) {
621
-			return false;
622
-		} catch (NotPermittedException $e) {
623
-			return false;
624
-		}
625
-		$names = array_map(function ($info) {
626
-			/** @var IFileInfo $info */
627
-			return $info->getName();
628
-		}, iterator_to_array($files));
629
-		return IteratorDirectory::wrap($names);
630
-	}
631
-
632
-	public function getDirectoryContent($directory): \Traversable {
633
-		try {
634
-			$files = $this->getFolderContents($directory);
635
-			foreach ($files as $file) {
636
-				yield $this->getMetaDataFromFileInfo($file);
637
-			}
638
-		} catch (NotFoundException $e) {
639
-			return;
640
-		} catch (NotPermittedException $e) {
641
-			return;
642
-		}
643
-	}
644
-
645
-	public function filetype($path) {
646
-		try {
647
-			return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
648
-		} catch (\OCP\Files\NotFoundException $e) {
649
-			return false;
650
-		} catch (ForbiddenException $e) {
651
-			return false;
652
-		}
653
-	}
654
-
655
-	public function mkdir($path) {
656
-		$path = $this->buildPath($path);
657
-		try {
658
-			$this->share->mkdir($path);
659
-			return true;
660
-		} catch (ConnectException $e) {
661
-			$this->logger->logException($e, ['message' => 'Error while creating folder']);
662
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
663
-		} catch (Exception $e) {
664
-			return false;
665
-		}
666
-	}
667
-
668
-	public function file_exists($path) {
669
-		try {
670
-			$this->getFileInfo($path);
671
-			return true;
672
-		} catch (\OCP\Files\NotFoundException $e) {
673
-			return false;
674
-		} catch (ForbiddenException $e) {
675
-			return false;
676
-		} catch (ConnectException $e) {
677
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
678
-		}
679
-	}
680
-
681
-	public function isReadable($path) {
682
-		try {
683
-			$info = $this->getFileInfo($path);
684
-			return $this->showHidden || !$info->isHidden();
685
-		} catch (\OCP\Files\NotFoundException $e) {
686
-			return false;
687
-		} catch (ForbiddenException $e) {
688
-			return false;
689
-		}
690
-	}
691
-
692
-	public function isUpdatable($path) {
693
-		try {
694
-			$info = $this->getFileInfo($path);
695
-			// following windows behaviour for read-only folders: they can be written into
696
-			// (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
697
-			return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory());
698
-		} catch (\OCP\Files\NotFoundException $e) {
699
-			return false;
700
-		} catch (ForbiddenException $e) {
701
-			return false;
702
-		}
703
-	}
704
-
705
-	public function isDeletable($path) {
706
-		try {
707
-			$info = $this->getFileInfo($path);
708
-			return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
709
-		} catch (\OCP\Files\NotFoundException $e) {
710
-			return false;
711
-		} catch (ForbiddenException $e) {
712
-			return false;
713
-		}
714
-	}
715
-
716
-	/**
717
-	 * check if smbclient is installed
718
-	 */
719
-	public static function checkDependencies() {
720
-		return (
721
-			(bool)\OC_Helper::findBinaryPath('smbclient')
722
-			|| NativeServer::available(new System())
723
-		) ? true : ['smbclient'];
724
-	}
725
-
726
-	/**
727
-	 * Test a storage for availability
728
-	 *
729
-	 * @return bool
730
-	 */
731
-	public function test() {
732
-		try {
733
-			return parent::test();
734
-		} catch (StorageAuthException $e) {
735
-			return false;
736
-		} catch (ForbiddenException $e) {
737
-			return false;
738
-		} catch (Exception $e) {
739
-			$this->logger->logException($e);
740
-			return false;
741
-		}
742
-	}
743
-
744
-	public function listen($path, callable $callback) {
745
-		$this->notify($path)->listen(function (IChange $change) use ($callback) {
746
-			if ($change instanceof IRenameChange) {
747
-				return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
748
-			} else {
749
-				return $callback($change->getType(), $change->getPath());
750
-			}
751
-		});
752
-	}
753
-
754
-	public function notify($path) {
755
-		$path = '/' . ltrim($path, '/');
756
-		$shareNotifyHandler = $this->share->notify($this->buildPath($path));
757
-		return new SMBNotifyHandler($shareNotifyHandler, $this->root);
758
-	}
72
+    /**
73
+     * @var \Icewind\SMB\IServer
74
+     */
75
+    protected $server;
76
+
77
+    /**
78
+     * @var \Icewind\SMB\IShare
79
+     */
80
+    protected $share;
81
+
82
+    /**
83
+     * @var string
84
+     */
85
+    protected $root;
86
+
87
+    /** @var CappedMemoryCache<IFileInfo> */
88
+    protected CappedMemoryCache $statCache;
89
+
90
+    /** @var ILogger */
91
+    protected $logger;
92
+
93
+    /** @var bool */
94
+    protected $showHidden;
95
+
96
+    /** @var bool */
97
+    protected $checkAcl;
98
+
99
+    public function __construct($params) {
100
+        if (!isset($params['host'])) {
101
+            throw new \Exception('Invalid configuration, no host provided');
102
+        }
103
+
104
+        if (isset($params['auth'])) {
105
+            $auth = $params['auth'];
106
+        } elseif (isset($params['user']) && isset($params['password']) && isset($params['share'])) {
107
+            [$workgroup, $user] = $this->splitUser($params['user']);
108
+            $auth = new BasicAuth($user, $workgroup, $params['password']);
109
+        } else {
110
+            throw new \Exception('Invalid configuration, no credentials provided');
111
+        }
112
+
113
+        if (isset($params['logger'])) {
114
+            $this->logger = $params['logger'];
115
+        } else {
116
+            $this->logger = \OC::$server->getLogger();
117
+        }
118
+
119
+        $options = new Options();
120
+        if (isset($params['timeout'])) {
121
+            $timeout = (int)$params['timeout'];
122
+            if ($timeout > 0) {
123
+                $options->setTimeout($timeout);
124
+            }
125
+        }
126
+        $serverFactory = new ServerFactory($options);
127
+        $this->server = $serverFactory->createServer($params['host'], $auth);
128
+        $this->share = $this->server->getShare(trim($params['share'], '/'));
129
+
130
+        $this->root = $params['root'] ?? '/';
131
+        $this->root = '/' . ltrim($this->root, '/');
132
+        $this->root = rtrim($this->root, '/') . '/';
133
+
134
+        $this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
135
+        $this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
136
+
137
+        $this->statCache = new CappedMemoryCache();
138
+        parent::__construct($params);
139
+    }
140
+
141
+    private function splitUser($user) {
142
+        if (strpos($user, '/')) {
143
+            return explode('/', $user, 2);
144
+        } elseif (strpos($user, '\\')) {
145
+            return explode('\\', $user);
146
+        } else {
147
+            return [null, $user];
148
+        }
149
+    }
150
+
151
+    /**
152
+     * @return string
153
+     */
154
+    public function getId() {
155
+        // FIXME: double slash to keep compatible with the old storage ids,
156
+        // failure to do so will lead to creation of a new storage id and
157
+        // loss of shares from the storage
158
+        return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
159
+    }
160
+
161
+    /**
162
+     * @param string $path
163
+     * @return string
164
+     */
165
+    protected function buildPath($path) {
166
+        return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
167
+    }
168
+
169
+    protected function relativePath($fullPath) {
170
+        if ($fullPath === $this->root) {
171
+            return '';
172
+        } elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
173
+            return substr($fullPath, strlen($this->root));
174
+        } else {
175
+            return null;
176
+        }
177
+    }
178
+
179
+    /**
180
+     * @param string $path
181
+     * @return IFileInfo
182
+     * @throws StorageAuthException
183
+     */
184
+    protected function getFileInfo($path) {
185
+        try {
186
+            $path = $this->buildPath($path);
187
+            $cached = $this->statCache[$path] ?? null;
188
+            if ($cached instanceof IFileInfo) {
189
+                return $cached;
190
+            } else {
191
+                $stat = $this->share->stat($path);
192
+                $this->statCache[$path] = $stat;
193
+                return $stat;
194
+            }
195
+        } catch (ConnectException $e) {
196
+            $this->throwUnavailable($e);
197
+        } catch (NotFoundException $e) {
198
+            throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
199
+        } catch (ForbiddenException $e) {
200
+            // with php-smbclient, this exception is thrown when the provided password is invalid.
201
+            // Possible is also ForbiddenException with a different error code, so we check it.
202
+            if ($e->getCode() === 1) {
203
+                $this->throwUnavailable($e);
204
+            }
205
+            throw new \OCP\Files\ForbiddenException($e->getMessage(), false, $e);
206
+        }
207
+    }
208
+
209
+    /**
210
+     * @param \Exception $e
211
+     * @return never
212
+     * @throws StorageAuthException
213
+     */
214
+    protected function throwUnavailable(\Exception $e) {
215
+        $this->logger->logException($e, ['message' => 'Error while getting file info']);
216
+        throw new StorageAuthException($e->getMessage(), $e);
217
+    }
218
+
219
+    /**
220
+     * get the acl from fileinfo that is relevant for the configured user
221
+     *
222
+     * @param IFileInfo $file
223
+     * @return ACL|null
224
+     */
225
+    private function getACL(IFileInfo $file): ?ACL {
226
+        $acls = $file->getAcls();
227
+        foreach ($acls as $user => $acl) {
228
+            [, $user] = $this->splitUser($user); // strip domain
229
+            if ($user === $this->server->getAuth()->getUsername()) {
230
+                return $acl;
231
+            }
232
+        }
233
+
234
+        return null;
235
+    }
236
+
237
+    /**
238
+     * @param string $path
239
+     * @return \Generator<IFileInfo>
240
+     * @throws StorageNotAvailableException
241
+     */
242
+    protected function getFolderContents($path): iterable {
243
+        try {
244
+            $path = ltrim($this->buildPath($path), '/');
245
+            try {
246
+                $files = $this->share->dir($path);
247
+            } catch (ForbiddenException $e) {
248
+                $this->logger->critical($e->getMessage(), ['exception' => $e]);
249
+                throw new NotPermittedException();
250
+            } catch (InvalidTypeException $e) {
251
+                return;
252
+            }
253
+            foreach ($files as $file) {
254
+                $this->statCache[$path . '/' . $file->getName()] = $file;
255
+            }
256
+
257
+            foreach ($files as $file) {
258
+                try {
259
+                    // the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
260
+                    // so we trigger the below exceptions where applicable
261
+                    $hide = $file->isHidden() && !$this->showHidden;
262
+
263
+                    if ($this->checkAcl && $acl = $this->getACL($file)) {
264
+                        // if there is no explicit deny, we assume it's allowed
265
+                        // this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
266
+                        // additionally, it's better to have false negatives here then false positives
267
+                        if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
268
+                            $this->logger->debug('Hiding non readable entry ' . $file->getName());
269
+                            continue;
270
+                        }
271
+                    }
272
+
273
+                    if ($hide) {
274
+                        $this->logger->debug('hiding hidden file ' . $file->getName());
275
+                    }
276
+                    if (!$hide) {
277
+                        yield $file;
278
+                    }
279
+                } catch (ForbiddenException $e) {
280
+                    $this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);
281
+                } catch (NotFoundException $e) {
282
+                    $this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);
283
+                }
284
+            }
285
+        } catch (ConnectException $e) {
286
+            $this->logger->logException($e, ['message' => 'Error while getting folder content']);
287
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
288
+        } catch (NotFoundException $e) {
289
+            throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
290
+        }
291
+    }
292
+
293
+    /**
294
+     * @param IFileInfo $info
295
+     * @return array
296
+     */
297
+    protected function formatInfo($info) {
298
+        $result = [
299
+            'size' => $info->getSize(),
300
+            'mtime' => $info->getMTime(),
301
+        ];
302
+        if ($info->isDirectory()) {
303
+            $result['type'] = 'dir';
304
+        } else {
305
+            $result['type'] = 'file';
306
+        }
307
+        return $result;
308
+    }
309
+
310
+    /**
311
+     * Rename the files. If the source or the target is the root, the rename won't happen.
312
+     *
313
+     * @param string $source the old name of the path
314
+     * @param string $target the new name of the path
315
+     * @return bool true if the rename is successful, false otherwise
316
+     */
317
+    public function rename($source, $target, $retry = true): bool {
318
+        if ($this->isRootDir($source) || $this->isRootDir($target)) {
319
+            return false;
320
+        }
321
+
322
+        $absoluteSource = $this->buildPath($source);
323
+        $absoluteTarget = $this->buildPath($target);
324
+        try {
325
+            $result = $this->share->rename($absoluteSource, $absoluteTarget);
326
+        } catch (AlreadyExistsException $e) {
327
+            if ($retry) {
328
+                $this->remove($target);
329
+                $result = $this->share->rename($absoluteSource, $absoluteTarget);
330
+            } else {
331
+                $this->logger->logException($e, ['level' => ILogger::WARN]);
332
+                return false;
333
+            }
334
+        } catch (InvalidArgumentException $e) {
335
+            if ($retry) {
336
+                $this->remove($target);
337
+                $result = $this->share->rename($absoluteSource, $absoluteTarget);
338
+            } else {
339
+                $this->logger->logException($e, ['level' => ILogger::WARN]);
340
+                return false;
341
+            }
342
+        } catch (\Exception $e) {
343
+            $this->logger->logException($e, ['level' => ILogger::WARN]);
344
+            return false;
345
+        }
346
+        unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
347
+        return $result;
348
+    }
349
+
350
+    public function stat($path, $retry = true) {
351
+        try {
352
+            $result = $this->formatInfo($this->getFileInfo($path));
353
+        } catch (ForbiddenException $e) {
354
+            return false;
355
+        } catch (\OCP\Files\NotFoundException $e) {
356
+            return false;
357
+        } catch (TimedOutException $e) {
358
+            if ($retry) {
359
+                return $this->stat($path, false);
360
+            } else {
361
+                throw $e;
362
+            }
363
+        }
364
+        if ($this->remoteIsShare() && $this->isRootDir($path)) {
365
+            $result['mtime'] = $this->shareMTime();
366
+        }
367
+        return $result;
368
+    }
369
+
370
+    /**
371
+     * get the best guess for the modification time of the share
372
+     *
373
+     * @return int
374
+     */
375
+    private function shareMTime() {
376
+        $highestMTime = 0;
377
+        $files = $this->share->dir($this->root);
378
+        foreach ($files as $fileInfo) {
379
+            try {
380
+                if ($fileInfo->getMTime() > $highestMTime) {
381
+                    $highestMTime = $fileInfo->getMTime();
382
+                }
383
+            } catch (NotFoundException $e) {
384
+                // Ignore this, can happen on unavailable DFS shares
385
+            } catch (ForbiddenException $e) {
386
+                // Ignore this too - it's a symlink
387
+            }
388
+        }
389
+        return $highestMTime;
390
+    }
391
+
392
+    /**
393
+     * Check if the path is our root dir (not the smb one)
394
+     *
395
+     * @param string $path the path
396
+     * @return bool
397
+     */
398
+    private function isRootDir($path) {
399
+        return $path === '' || $path === '/' || $path === '.';
400
+    }
401
+
402
+    /**
403
+     * Check if our root points to a smb share
404
+     *
405
+     * @return bool true if our root points to a share false otherwise
406
+     */
407
+    private function remoteIsShare() {
408
+        return $this->share->getName() && (!$this->root || $this->root === '/');
409
+    }
410
+
411
+    /**
412
+     * @param string $path
413
+     * @return bool
414
+     */
415
+    public function unlink($path) {
416
+        if ($this->isRootDir($path)) {
417
+            return false;
418
+        }
419
+
420
+        try {
421
+            if ($this->is_dir($path)) {
422
+                return $this->rmdir($path);
423
+            } else {
424
+                $path = $this->buildPath($path);
425
+                unset($this->statCache[$path]);
426
+                $this->share->del($path);
427
+                return true;
428
+            }
429
+        } catch (NotFoundException $e) {
430
+            return false;
431
+        } catch (ForbiddenException $e) {
432
+            return false;
433
+        } catch (ConnectException $e) {
434
+            $this->logger->logException($e, ['message' => 'Error while deleting file']);
435
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
436
+        }
437
+    }
438
+
439
+    /**
440
+     * check if a file or folder has been updated since $time
441
+     *
442
+     * @param string $path
443
+     * @param int $time
444
+     * @return bool
445
+     */
446
+    public function hasUpdated($path, $time) {
447
+        if (!$path and $this->root === '/') {
448
+            // mtime doesn't work for shares, but giving the nature of the backend,
449
+            // doing a full update is still just fast enough
450
+            return true;
451
+        } else {
452
+            $actualTime = $this->filemtime($path);
453
+            return $actualTime > $time;
454
+        }
455
+    }
456
+
457
+    /**
458
+     * @param string $path
459
+     * @param string $mode
460
+     * @return resource|bool
461
+     */
462
+    public function fopen($path, $mode) {
463
+        $fullPath = $this->buildPath($path);
464
+        try {
465
+            switch ($mode) {
466
+                case 'r':
467
+                case 'rb':
468
+                    if (!$this->file_exists($path)) {
469
+                        return false;
470
+                    }
471
+                    return $this->share->read($fullPath);
472
+                case 'w':
473
+                case 'wb':
474
+                    $source = $this->share->write($fullPath);
475
+                    return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
476
+                        unset($this->statCache[$fullPath]);
477
+                    });
478
+                case 'a':
479
+                case 'ab':
480
+                case 'r+':
481
+                case 'w+':
482
+                case 'wb+':
483
+                case 'a+':
484
+                case 'x':
485
+                case 'x+':
486
+                case 'c':
487
+                case 'c+':
488
+                    //emulate these
489
+                    if (strrpos($path, '.') !== false) {
490
+                        $ext = substr($path, strrpos($path, '.'));
491
+                    } else {
492
+                        $ext = '';
493
+                    }
494
+                    if ($this->file_exists($path)) {
495
+                        if (!$this->isUpdatable($path)) {
496
+                            return false;
497
+                        }
498
+                        $tmpFile = $this->getCachedFile($path);
499
+                    } else {
500
+                        if (!$this->isCreatable(dirname($path))) {
501
+                            return false;
502
+                        }
503
+                        $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
504
+                    }
505
+                    $source = fopen($tmpFile, $mode);
506
+                    $share = $this->share;
507
+                    return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
508
+                        unset($this->statCache[$fullPath]);
509
+                        $share->put($tmpFile, $fullPath);
510
+                        unlink($tmpFile);
511
+                    });
512
+            }
513
+            return false;
514
+        } catch (NotFoundException $e) {
515
+            return false;
516
+        } catch (ForbiddenException $e) {
517
+            return false;
518
+        } catch (OutOfSpaceException $e) {
519
+            throw new EntityTooLargeException("not enough available space to create file", 0, $e);
520
+        } catch (ConnectException $e) {
521
+            $this->logger->logException($e, ['message' => 'Error while opening file']);
522
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
523
+        }
524
+    }
525
+
526
+    public function rmdir($path) {
527
+        if ($this->isRootDir($path)) {
528
+            return false;
529
+        }
530
+
531
+        try {
532
+            $this->statCache = new CappedMemoryCache();
533
+            $content = $this->share->dir($this->buildPath($path));
534
+            foreach ($content as $file) {
535
+                if ($file->isDirectory()) {
536
+                    $this->rmdir($path . '/' . $file->getName());
537
+                } else {
538
+                    $this->share->del($file->getPath());
539
+                }
540
+            }
541
+            $this->share->rmdir($this->buildPath($path));
542
+            return true;
543
+        } catch (NotFoundException $e) {
544
+            return false;
545
+        } catch (ForbiddenException $e) {
546
+            return false;
547
+        } catch (ConnectException $e) {
548
+            $this->logger->logException($e, ['message' => 'Error while removing folder']);
549
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
550
+        }
551
+    }
552
+
553
+    public function touch($path, $mtime = null) {
554
+        try {
555
+            if (!$this->file_exists($path)) {
556
+                $fh = $this->share->write($this->buildPath($path));
557
+                fclose($fh);
558
+                return true;
559
+            }
560
+            return false;
561
+        } catch (OutOfSpaceException $e) {
562
+            throw new EntityTooLargeException("not enough available space to create file", 0, $e);
563
+        } catch (ConnectException $e) {
564
+            $this->logger->logException($e, ['message' => 'Error while creating file']);
565
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
566
+        }
567
+    }
568
+
569
+    public function getMetaData($path) {
570
+        try {
571
+            $fileInfo = $this->getFileInfo($path);
572
+        } catch (\OCP\Files\NotFoundException $e) {
573
+            return null;
574
+        } catch (ForbiddenException $e) {
575
+            return null;
576
+        }
577
+        if (!$fileInfo) {
578
+            return null;
579
+        }
580
+
581
+        return $this->getMetaDataFromFileInfo($fileInfo);
582
+    }
583
+
584
+    private function getMetaDataFromFileInfo(IFileInfo $fileInfo) {
585
+        $permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
586
+
587
+        if (
588
+            !$fileInfo->isReadOnly() || $fileInfo->isDirectory()
589
+        ) {
590
+            $permissions += Constants::PERMISSION_DELETE;
591
+            $permissions += Constants::PERMISSION_UPDATE;
592
+            if ($fileInfo->isDirectory()) {
593
+                $permissions += Constants::PERMISSION_CREATE;
594
+            }
595
+        }
596
+
597
+        $data = [];
598
+        if ($fileInfo->isDirectory()) {
599
+            $data['mimetype'] = 'httpd/unix-directory';
600
+        } else {
601
+            $data['mimetype'] = \OC::$server->getMimeTypeDetector()->detectPath($fileInfo->getPath());
602
+        }
603
+        $data['mtime'] = $fileInfo->getMTime();
604
+        if ($fileInfo->isDirectory()) {
605
+            $data['size'] = -1; //unknown
606
+        } else {
607
+            $data['size'] = $fileInfo->getSize();
608
+        }
609
+        $data['etag'] = $this->getETag($fileInfo->getPath());
610
+        $data['storage_mtime'] = $data['mtime'];
611
+        $data['permissions'] = $permissions;
612
+        $data['name'] = $fileInfo->getName();
613
+
614
+        return $data;
615
+    }
616
+
617
+    public function opendir($path) {
618
+        try {
619
+            $files = $this->getFolderContents($path);
620
+        } catch (NotFoundException $e) {
621
+            return false;
622
+        } catch (NotPermittedException $e) {
623
+            return false;
624
+        }
625
+        $names = array_map(function ($info) {
626
+            /** @var IFileInfo $info */
627
+            return $info->getName();
628
+        }, iterator_to_array($files));
629
+        return IteratorDirectory::wrap($names);
630
+    }
631
+
632
+    public function getDirectoryContent($directory): \Traversable {
633
+        try {
634
+            $files = $this->getFolderContents($directory);
635
+            foreach ($files as $file) {
636
+                yield $this->getMetaDataFromFileInfo($file);
637
+            }
638
+        } catch (NotFoundException $e) {
639
+            return;
640
+        } catch (NotPermittedException $e) {
641
+            return;
642
+        }
643
+    }
644
+
645
+    public function filetype($path) {
646
+        try {
647
+            return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
648
+        } catch (\OCP\Files\NotFoundException $e) {
649
+            return false;
650
+        } catch (ForbiddenException $e) {
651
+            return false;
652
+        }
653
+    }
654
+
655
+    public function mkdir($path) {
656
+        $path = $this->buildPath($path);
657
+        try {
658
+            $this->share->mkdir($path);
659
+            return true;
660
+        } catch (ConnectException $e) {
661
+            $this->logger->logException($e, ['message' => 'Error while creating folder']);
662
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
663
+        } catch (Exception $e) {
664
+            return false;
665
+        }
666
+    }
667
+
668
+    public function file_exists($path) {
669
+        try {
670
+            $this->getFileInfo($path);
671
+            return true;
672
+        } catch (\OCP\Files\NotFoundException $e) {
673
+            return false;
674
+        } catch (ForbiddenException $e) {
675
+            return false;
676
+        } catch (ConnectException $e) {
677
+            throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
678
+        }
679
+    }
680
+
681
+    public function isReadable($path) {
682
+        try {
683
+            $info = $this->getFileInfo($path);
684
+            return $this->showHidden || !$info->isHidden();
685
+        } catch (\OCP\Files\NotFoundException $e) {
686
+            return false;
687
+        } catch (ForbiddenException $e) {
688
+            return false;
689
+        }
690
+    }
691
+
692
+    public function isUpdatable($path) {
693
+        try {
694
+            $info = $this->getFileInfo($path);
695
+            // following windows behaviour for read-only folders: they can be written into
696
+            // (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
697
+            return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory());
698
+        } catch (\OCP\Files\NotFoundException $e) {
699
+            return false;
700
+        } catch (ForbiddenException $e) {
701
+            return false;
702
+        }
703
+    }
704
+
705
+    public function isDeletable($path) {
706
+        try {
707
+            $info = $this->getFileInfo($path);
708
+            return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
709
+        } catch (\OCP\Files\NotFoundException $e) {
710
+            return false;
711
+        } catch (ForbiddenException $e) {
712
+            return false;
713
+        }
714
+    }
715
+
716
+    /**
717
+     * check if smbclient is installed
718
+     */
719
+    public static function checkDependencies() {
720
+        return (
721
+            (bool)\OC_Helper::findBinaryPath('smbclient')
722
+            || NativeServer::available(new System())
723
+        ) ? true : ['smbclient'];
724
+    }
725
+
726
+    /**
727
+     * Test a storage for availability
728
+     *
729
+     * @return bool
730
+     */
731
+    public function test() {
732
+        try {
733
+            return parent::test();
734
+        } catch (StorageAuthException $e) {
735
+            return false;
736
+        } catch (ForbiddenException $e) {
737
+            return false;
738
+        } catch (Exception $e) {
739
+            $this->logger->logException($e);
740
+            return false;
741
+        }
742
+    }
743
+
744
+    public function listen($path, callable $callback) {
745
+        $this->notify($path)->listen(function (IChange $change) use ($callback) {
746
+            if ($change instanceof IRenameChange) {
747
+                return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
748
+            } else {
749
+                return $callback($change->getType(), $change->getPath());
750
+            }
751
+        });
752
+    }
753
+
754
+    public function notify($path) {
755
+        $path = '/' . ltrim($path, '/');
756
+        $shareNotifyHandler = $this->share->notify($this->buildPath($path));
757
+        return new SMBNotifyHandler($shareNotifyHandler, $this->root);
758
+    }
759 759
 }
Please login to merge, or discard this patch.
Spacing   +24 added lines, -24 removed lines patch added patch discarded remove patch
@@ -118,7 +118,7 @@  discard block
 block discarded – undo
118 118
 
119 119
 		$options = new Options();
120 120
 		if (isset($params['timeout'])) {
121
-			$timeout = (int)$params['timeout'];
121
+			$timeout = (int) $params['timeout'];
122 122
 			if ($timeout > 0) {
123 123
 				$options->setTimeout($timeout);
124 124
 			}
@@ -128,8 +128,8 @@  discard block
 block discarded – undo
128 128
 		$this->share = $this->server->getShare(trim($params['share'], '/'));
129 129
 
130 130
 		$this->root = $params['root'] ?? '/';
131
-		$this->root = '/' . ltrim($this->root, '/');
132
-		$this->root = rtrim($this->root, '/') . '/';
131
+		$this->root = '/'.ltrim($this->root, '/');
132
+		$this->root = rtrim($this->root, '/').'/';
133 133
 
134 134
 		$this->showHidden = isset($params['show_hidden']) && $params['show_hidden'];
135 135
 		$this->checkAcl = isset($params['check_acl']) && $params['check_acl'];
@@ -155,7 +155,7 @@  discard block
 block discarded – undo
155 155
 		// FIXME: double slash to keep compatible with the old storage ids,
156 156
 		// failure to do so will lead to creation of a new storage id and
157 157
 		// loss of shares from the storage
158
-		return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
158
+		return 'smb::'.$this->server->getAuth()->getUsername().'@'.$this->server->getHost().'//'.$this->share->getName().'/'.$this->root;
159 159
 	}
160 160
 
161 161
 	/**
@@ -163,7 +163,7 @@  discard block
 block discarded – undo
163 163
 	 * @return string
164 164
 	 */
165 165
 	protected function buildPath($path) {
166
-		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
166
+		return Filesystem::normalizePath($this->root.'/'.$path, true, false, true);
167 167
 	}
168 168
 
169 169
 	protected function relativePath($fullPath) {
@@ -251,7 +251,7 @@  discard block
 block discarded – undo
251 251
 				return;
252 252
 			}
253 253
 			foreach ($files as $file) {
254
-				$this->statCache[$path . '/' . $file->getName()] = $file;
254
+				$this->statCache[$path.'/'.$file->getName()] = $file;
255 255
 			}
256 256
 
257 257
 			foreach ($files as $file) {
@@ -265,26 +265,26 @@  discard block
 block discarded – undo
265 265
 						// this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
266 266
 						// additionally, it's better to have false negatives here then false positives
267 267
 						if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
268
-							$this->logger->debug('Hiding non readable entry ' . $file->getName());
268
+							$this->logger->debug('Hiding non readable entry '.$file->getName());
269 269
 							continue;
270 270
 						}
271 271
 					}
272 272
 
273 273
 					if ($hide) {
274
-						$this->logger->debug('hiding hidden file ' . $file->getName());
274
+						$this->logger->debug('hiding hidden file '.$file->getName());
275 275
 					}
276 276
 					if (!$hide) {
277 277
 						yield $file;
278 278
 					}
279 279
 				} catch (ForbiddenException $e) {
280
-					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry ' . $file->getName()]);
280
+					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding forbidden entry '.$file->getName()]);
281 281
 				} catch (NotFoundException $e) {
282
-					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry ' . $file->getName()]);
282
+					$this->logger->logException($e, ['level' => ILogger::DEBUG, 'message' => 'Hiding not found entry '.$file->getName()]);
283 283
 				}
284 284
 			}
285 285
 		} catch (ConnectException $e) {
286 286
 			$this->logger->logException($e, ['message' => 'Error while getting folder content']);
287
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
287
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
288 288
 		} catch (NotFoundException $e) {
289 289
 			throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
290 290
 		}
@@ -432,7 +432,7 @@  discard block
 block discarded – undo
432 432
 			return false;
433 433
 		} catch (ConnectException $e) {
434 434
 			$this->logger->logException($e, ['message' => 'Error while deleting file']);
435
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
435
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
436 436
 		}
437 437
 	}
438 438
 
@@ -472,7 +472,7 @@  discard block
 block discarded – undo
472 472
 				case 'w':
473 473
 				case 'wb':
474 474
 					$source = $this->share->write($fullPath);
475
-					return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
475
+					return CallBackWrapper::wrap($source, null, null, function() use ($fullPath) {
476 476
 						unset($this->statCache[$fullPath]);
477 477
 					});
478 478
 				case 'a':
@@ -504,7 +504,7 @@  discard block
 block discarded – undo
504 504
 					}
505 505
 					$source = fopen($tmpFile, $mode);
506 506
 					$share = $this->share;
507
-					return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
507
+					return CallbackWrapper::wrap($source, null, null, function() use ($tmpFile, $fullPath, $share) {
508 508
 						unset($this->statCache[$fullPath]);
509 509
 						$share->put($tmpFile, $fullPath);
510 510
 						unlink($tmpFile);
@@ -519,7 +519,7 @@  discard block
 block discarded – undo
519 519
 			throw new EntityTooLargeException("not enough available space to create file", 0, $e);
520 520
 		} catch (ConnectException $e) {
521 521
 			$this->logger->logException($e, ['message' => 'Error while opening file']);
522
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
522
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
523 523
 		}
524 524
 	}
525 525
 
@@ -533,7 +533,7 @@  discard block
 block discarded – undo
533 533
 			$content = $this->share->dir($this->buildPath($path));
534 534
 			foreach ($content as $file) {
535 535
 				if ($file->isDirectory()) {
536
-					$this->rmdir($path . '/' . $file->getName());
536
+					$this->rmdir($path.'/'.$file->getName());
537 537
 				} else {
538 538
 					$this->share->del($file->getPath());
539 539
 				}
@@ -546,7 +546,7 @@  discard block
 block discarded – undo
546 546
 			return false;
547 547
 		} catch (ConnectException $e) {
548 548
 			$this->logger->logException($e, ['message' => 'Error while removing folder']);
549
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
549
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
550 550
 		}
551 551
 	}
552 552
 
@@ -562,7 +562,7 @@  discard block
 block discarded – undo
562 562
 			throw new EntityTooLargeException("not enough available space to create file", 0, $e);
563 563
 		} catch (ConnectException $e) {
564 564
 			$this->logger->logException($e, ['message' => 'Error while creating file']);
565
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
565
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
566 566
 		}
567 567
 	}
568 568
 
@@ -622,7 +622,7 @@  discard block
 block discarded – undo
622 622
 		} catch (NotPermittedException $e) {
623 623
 			return false;
624 624
 		}
625
-		$names = array_map(function ($info) {
625
+		$names = array_map(function($info) {
626 626
 			/** @var IFileInfo $info */
627 627
 			return $info->getName();
628 628
 		}, iterator_to_array($files));
@@ -659,7 +659,7 @@  discard block
 block discarded – undo
659 659
 			return true;
660 660
 		} catch (ConnectException $e) {
661 661
 			$this->logger->logException($e, ['message' => 'Error while creating folder']);
662
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
662
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
663 663
 		} catch (Exception $e) {
664 664
 			return false;
665 665
 		}
@@ -674,7 +674,7 @@  discard block
 block discarded – undo
674 674
 		} catch (ForbiddenException $e) {
675 675
 			return false;
676 676
 		} catch (ConnectException $e) {
677
-			throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
677
+			throw new StorageNotAvailableException($e->getMessage(), (int) $e->getCode(), $e);
678 678
 		}
679 679
 	}
680 680
 
@@ -718,7 +718,7 @@  discard block
 block discarded – undo
718 718
 	 */
719 719
 	public static function checkDependencies() {
720 720
 		return (
721
-			(bool)\OC_Helper::findBinaryPath('smbclient')
721
+			(bool) \OC_Helper::findBinaryPath('smbclient')
722 722
 			|| NativeServer::available(new System())
723 723
 		) ? true : ['smbclient'];
724 724
 	}
@@ -742,7 +742,7 @@  discard block
 block discarded – undo
742 742
 	}
743 743
 
744 744
 	public function listen($path, callable $callback) {
745
-		$this->notify($path)->listen(function (IChange $change) use ($callback) {
745
+		$this->notify($path)->listen(function(IChange $change) use ($callback) {
746 746
 			if ($change instanceof IRenameChange) {
747 747
 				return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
748 748
 			} else {
@@ -752,7 +752,7 @@  discard block
 block discarded – undo
752 752
 	}
753 753
 
754 754
 	public function notify($path) {
755
-		$path = '/' . ltrim($path, '/');
755
+		$path = '/'.ltrim($path, '/');
756 756
 		$shareNotifyHandler = $this->share->notify($this->buildPath($path));
757 757
 		return new SMBNotifyHandler($shareNotifyHandler, $this->root);
758 758
 	}
Please login to merge, or discard this patch.