Completed
Push — master ( 959239...c1a9df )
by
unknown
38:58 queued 19:21
created
lib/public/Lock/LockedException.php 1 patch
Indentation   +53 added lines, -53 removed lines patch added patch discarded remove patch
@@ -14,64 +14,64 @@
 block discarded – undo
14 14
  * @since 8.1.0
15 15
  */
16 16
 class LockedException extends \Exception {
17
-	/**
18
-	 * Locked path
19
-	 *
20
-	 * @var string
21
-	 */
22
-	private $path;
17
+    /**
18
+     * Locked path
19
+     *
20
+     * @var string
21
+     */
22
+    private $path;
23 23
 
24
-	/** @var string|null */
25
-	private $existingLock;
24
+    /** @var string|null */
25
+    private $existingLock;
26 26
 
27
-	private ?string $readablePath;
27
+    private ?string $readablePath;
28 28
 
29
-	/**
30
-	 * LockedException constructor.
31
-	 *
32
-	 * @param string $path locked path
33
-	 * @param \Exception|null $previous previous exception for cascading
34
-	 * @param string $existingLock since 14.0.0
35
-	 * @param string $readablePath since 20.0.0
36
-	 * @since 8.1.0
37
-	 */
38
-	public function __construct(string $path, ?\Exception $previous = null, ?string $existingLock = null, ?string $readablePath = null) {
39
-		$this->readablePath = $readablePath;
40
-		if ($readablePath) {
41
-			$message = "\"$path\"(\"$readablePath\") is locked";
42
-		} else {
43
-			$message = '"' . $path . '" is locked';
44
-		}
45
-		$this->existingLock = $existingLock;
46
-		if ($existingLock) {
47
-			$message .= ', existing lock on file: ' . $existingLock;
48
-		}
49
-		parent::__construct($message, 0, $previous);
50
-		$this->path = $path;
51
-	}
29
+    /**
30
+     * LockedException constructor.
31
+     *
32
+     * @param string $path locked path
33
+     * @param \Exception|null $previous previous exception for cascading
34
+     * @param string $existingLock since 14.0.0
35
+     * @param string $readablePath since 20.0.0
36
+     * @since 8.1.0
37
+     */
38
+    public function __construct(string $path, ?\Exception $previous = null, ?string $existingLock = null, ?string $readablePath = null) {
39
+        $this->readablePath = $readablePath;
40
+        if ($readablePath) {
41
+            $message = "\"$path\"(\"$readablePath\") is locked";
42
+        } else {
43
+            $message = '"' . $path . '" is locked';
44
+        }
45
+        $this->existingLock = $existingLock;
46
+        if ($existingLock) {
47
+            $message .= ', existing lock on file: ' . $existingLock;
48
+        }
49
+        parent::__construct($message, 0, $previous);
50
+        $this->path = $path;
51
+    }
52 52
 
53
-	/**
54
-	 * @return string
55
-	 * @since 8.1.0
56
-	 */
57
-	public function getPath(): string {
58
-		return $this->path;
59
-	}
53
+    /**
54
+     * @return string
55
+     * @since 8.1.0
56
+     */
57
+    public function getPath(): string {
58
+        return $this->path;
59
+    }
60 60
 
61
-	/**
62
-	 * @return string
63
-	 * @since 19.0.0
64
-	 */
65
-	public function getExistingLock(): ?string {
66
-		return $this->existingLock;
67
-	}
61
+    /**
62
+     * @return string
63
+     * @since 19.0.0
64
+     */
65
+    public function getExistingLock(): ?string {
66
+        return $this->existingLock;
67
+    }
68 68
 
69
-	/**
70
-	 * @return ?string
71
-	 * @since 32.0.0
72
-	 */
73
-	public function getReadablePath(): ?string {
74
-		return $this->readablePath;
75
-	}
69
+    /**
70
+     * @return ?string
71
+     * @since 32.0.0
72
+     */
73
+    public function getReadablePath(): ?string {
74
+        return $this->readablePath;
75
+    }
76 76
 
77 77
 }
Please login to merge, or discard this patch.
lib/private/Files/Cache/Scanner.php 1 patch
Indentation   +587 added lines, -587 removed lines patch added patch discarded remove patch
@@ -33,591 +33,591 @@
 block discarded – undo
33 33
  * @package OC\Files\Cache
34 34
  */
35 35
 class Scanner extends BasicEmitter implements IScanner {
36
-	/**
37
-	 * @var \OC\Files\Storage\Storage $storage
38
-	 */
39
-	protected $storage;
40
-
41
-	/**
42
-	 * @var string $storageId
43
-	 */
44
-	protected $storageId;
45
-
46
-	/**
47
-	 * @var \OC\Files\Cache\Cache $cache
48
-	 */
49
-	protected $cache;
50
-
51
-	/**
52
-	 * @var boolean $cacheActive If true, perform cache operations, if false, do not affect cache
53
-	 */
54
-	protected $cacheActive;
55
-
56
-	/**
57
-	 * @var bool $useTransactions whether to use transactions
58
-	 */
59
-	protected $useTransactions = true;
60
-
61
-	/**
62
-	 * @var \OCP\Lock\ILockingProvider
63
-	 */
64
-	protected $lockingProvider;
65
-
66
-	protected IDBConnection $connection;
67
-
68
-	public function __construct(\OC\Files\Storage\Storage $storage) {
69
-		$this->storage = $storage;
70
-		$this->storageId = $this->storage->getId();
71
-		$this->cache = $storage->getCache();
72
-		/** @var SystemConfig $config */
73
-		$config = \OC::$server->get(SystemConfig::class);
74
-		$this->cacheActive = !$config->getValue('filesystem_cache_readonly', false);
75
-		$this->useTransactions = !$config->getValue('filescanner_no_transactions', false);
76
-		$this->lockingProvider = \OC::$server->get(ILockingProvider::class);
77
-		$this->connection = \OC::$server->get(IDBConnection::class);
78
-	}
79
-
80
-	/**
81
-	 * Whether to wrap the scanning of a folder in a database transaction
82
-	 * On default transactions are used
83
-	 *
84
-	 * @param bool $useTransactions
85
-	 */
86
-	public function setUseTransactions($useTransactions): void {
87
-		$this->useTransactions = $useTransactions;
88
-	}
89
-
90
-	/**
91
-	 * get all the metadata of a file or folder
92
-	 * *
93
-	 *
94
-	 * @param string $path
95
-	 * @return array|null an array of metadata of the file
96
-	 */
97
-	protected function getData($path) {
98
-		$data = $this->storage->getMetaData($path);
99
-		if (is_null($data)) {
100
-			\OC::$server->get(LoggerInterface::class)->debug("!!! Path '$path' is not accessible or present !!!", ['app' => 'core']);
101
-		}
102
-		return $data;
103
-	}
104
-
105
-	/**
106
-	 * scan a single file and store it in the cache
107
-	 *
108
-	 * @param string $file
109
-	 * @param int $reuseExisting
110
-	 * @param int $parentId
111
-	 * @param array|CacheEntry|null|false $cacheData existing data in the cache for the file to be scanned
112
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
113
-	 * @param array|null $data the metadata for the file, as returned by the storage
114
-	 * @return array|null an array of metadata of the scanned file
115
-	 * @throws \OCP\Lock\LockedException
116
-	 */
117
-	public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
118
-		if ($file !== '') {
119
-			try {
120
-				$this->storage->verifyPath(dirname($file), basename($file));
121
-			} catch (\Exception $e) {
122
-				return null;
123
-			}
124
-		}
125
-
126
-		// only proceed if $file is not a partial file, blacklist is handled by the storage
127
-		if (self::isPartialFile($file)) {
128
-			return null;
129
-		}
130
-
131
-		// acquire a lock
132
-		if ($lock) {
133
-			if ($this->storage->instanceOfStorage(ILockingStorage::class)) {
134
-				$this->storage->acquireLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
135
-			}
136
-		}
137
-
138
-		try {
139
-			$data = $data ?? $this->getData($file);
140
-		} catch (ForbiddenException $e) {
141
-			if ($lock) {
142
-				if ($this->storage->instanceOfStorage(ILockingStorage::class)) {
143
-					$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
144
-				}
145
-			}
146
-
147
-			return null;
148
-		}
149
-
150
-		try {
151
-			if ($data === null) {
152
-				$this->removeFromCache($file);
153
-			} else {
154
-				// pre-emit only if it was a file. By that we avoid counting/treating folders as files
155
-				if ($data['mimetype'] !== 'httpd/unix-directory') {
156
-					$this->emit('\OC\Files\Cache\Scanner', 'scanFile', [$file, $this->storageId]);
157
-					\OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', ['path' => $file, 'storage' => $this->storageId]);
158
-				}
159
-
160
-				$parent = dirname($file);
161
-				if ($parent === '.' || $parent === '/') {
162
-					$parent = '';
163
-				}
164
-				if ($parentId === -1) {
165
-					$parentId = $this->cache->getParentId($file);
166
-				}
167
-
168
-				// scan the parent if it's not in the cache (id -1) and the current file is not the root folder
169
-				if ($file && $parentId === -1) {
170
-					$parentData = $this->scanFile($parent);
171
-					if ($parentData === null) {
172
-						return null;
173
-					}
174
-
175
-					$parentId = $parentData['fileid'];
176
-				}
177
-				if ($parent) {
178
-					$data['parent'] = $parentId;
179
-				}
180
-
181
-				$cacheData = $cacheData ?? $this->cache->get($file);
182
-				if ($reuseExisting && $cacheData !== false && isset($cacheData['fileid'])) {
183
-					// prevent empty etag
184
-					$etag = empty($cacheData['etag']) ? $data['etag'] : $cacheData['etag'];
185
-					$fileId = $cacheData['fileid'];
186
-					$data['fileid'] = $fileId;
187
-					// only reuse data if the file hasn't explicitly changed
188
-					$mtimeUnchanged = isset($data['storage_mtime']) && isset($cacheData['storage_mtime']) && $data['storage_mtime'] === $cacheData['storage_mtime'];
189
-					// if the folder is marked as unscanned, never reuse etags
190
-					if ($mtimeUnchanged && $cacheData['size'] !== -1) {
191
-						$data['mtime'] = $cacheData['mtime'];
192
-						if (($reuseExisting & self::REUSE_SIZE) && ($data['size'] === -1)) {
193
-							$data['size'] = $cacheData['size'];
194
-						}
195
-						if ($reuseExisting & self::REUSE_ETAG && !$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
196
-							$data['etag'] = $etag;
197
-						}
198
-					}
199
-
200
-					// we only updated unencrypted_size if it's already set
201
-					if (isset($cacheData['unencrypted_size']) && $cacheData['unencrypted_size'] === 0) {
202
-						unset($data['unencrypted_size']);
203
-					}
204
-
205
-					/**
206
-					 * Only update metadata that has changed.
207
-					 * i.e. get all the values in $data that are not present in the cache already
208
-					 *
209
-					 * We need the OC implementation for usage of "getData" method below.
210
-					 * @var \OC\Files\Cache\CacheEntry $cacheData
211
-					 */
212
-					$newData = $this->array_diff_assoc_multi($data, $cacheData->getData());
213
-
214
-					// make it known to the caller that etag has been changed and needs propagation
215
-					if (isset($newData['etag'])) {
216
-						$data['etag_changed'] = true;
217
-					}
218
-				} else {
219
-					unset($data['unencrypted_size']);
220
-					$newData = $data;
221
-					$fileId = -1;
222
-				}
223
-				if (!empty($newData)) {
224
-					// Reset the checksum if the data has changed
225
-					$newData['checksum'] = '';
226
-					$newData['parent'] = $parentId;
227
-					$data['fileid'] = $this->addToCache($file, $newData, $fileId);
228
-				}
229
-
230
-				if ($cacheData !== false) {
231
-					$data['oldSize'] = $cacheData['size'] ?? 0;
232
-					$data['encrypted'] = $cacheData['encrypted'] ?? false;
233
-				}
234
-
235
-				// post-emit only if it was a file. By that we avoid counting/treating folders as files
236
-				if ($data['mimetype'] !== 'httpd/unix-directory') {
237
-					$this->emit('\OC\Files\Cache\Scanner', 'postScanFile', [$file, $this->storageId]);
238
-					\OC_Hook::emit('\OC\Files\Cache\Scanner', 'post_scan_file', ['path' => $file, 'storage' => $this->storageId]);
239
-				}
240
-			}
241
-		} finally {
242
-			// release the acquired lock
243
-			if ($lock && $this->storage->instanceOfStorage(ILockingStorage::class)) {
244
-				$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
245
-			}
246
-		}
247
-
248
-		return $data;
249
-	}
250
-
251
-	protected function removeFromCache($path) {
252
-		\OC_Hook::emit('Scanner', 'removeFromCache', ['file' => $path]);
253
-		$this->emit('\OC\Files\Cache\Scanner', 'removeFromCache', [$path]);
254
-		if ($this->cacheActive) {
255
-			$this->cache->remove($path);
256
-		}
257
-	}
258
-
259
-	/**
260
-	 * @param string $path
261
-	 * @param array $data
262
-	 * @param int $fileId
263
-	 * @return int the id of the added file
264
-	 */
265
-	protected function addToCache($path, $data, $fileId = -1) {
266
-		if (isset($data['scan_permissions'])) {
267
-			$data['permissions'] = $data['scan_permissions'];
268
-		}
269
-		\OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
270
-		$this->emit('\OC\Files\Cache\Scanner', 'addToCache', [$path, $this->storageId, $data, $fileId]);
271
-		if ($this->cacheActive) {
272
-			if ($fileId !== -1) {
273
-				$this->cache->update($fileId, $data);
274
-				return $fileId;
275
-			} else {
276
-				return $this->cache->insert($path, $data);
277
-			}
278
-		} else {
279
-			return -1;
280
-		}
281
-	}
282
-
283
-	/**
284
-	 * @param string $path
285
-	 * @param array $data
286
-	 * @param int $fileId
287
-	 */
288
-	protected function updateCache($path, $data, $fileId = -1) {
289
-		\OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
290
-		$this->emit('\OC\Files\Cache\Scanner', 'updateCache', [$path, $this->storageId, $data]);
291
-		if ($this->cacheActive) {
292
-			if ($fileId !== -1) {
293
-				$this->cache->update($fileId, $data);
294
-			} else {
295
-				$this->cache->put($path, $data);
296
-			}
297
-		}
298
-	}
299
-
300
-	/**
301
-	 * scan a folder and all it's children
302
-	 *
303
-	 * @param string $path
304
-	 * @param bool $recursive
305
-	 * @param int $reuse
306
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
307
-	 * @return array|null an array of the meta data of the scanned file or folder
308
-	 */
309
-	public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
310
-		if ($reuse === -1) {
311
-			$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
312
-		}
313
-
314
-		if ($lock && $this->storage->instanceOfStorage(ILockingStorage::class)) {
315
-			$this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
316
-			$this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
317
-		}
318
-
319
-		try {
320
-			$data = $this->scanFile($path, $reuse, -1, lock: $lock);
321
-
322
-			if ($data !== null && $data['mimetype'] === 'httpd/unix-directory') {
323
-				$size = $this->scanChildren($path, $recursive, $reuse, $data['fileid'], $lock, $data['size']);
324
-				$data['size'] = $size;
325
-			}
326
-		} catch (NotFoundException $e) {
327
-			$this->removeFromCache($path);
328
-			return null;
329
-		} finally {
330
-			if ($lock && $this->storage->instanceOfStorage(ILockingStorage::class)) {
331
-				$this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
332
-				$this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
333
-			}
334
-		}
335
-		return $data;
336
-	}
337
-
338
-	/**
339
-	 * Compares $array1 against $array2 and returns all the values in $array1 that are not in $array2
340
-	 * Note this is a one-way check - i.e. we don't care about things that are in $array2 that aren't in $array1
341
-	 *
342
-	 * Supports multi-dimensional arrays
343
-	 * Also checks keys/indexes
344
-	 * Comparisons are strict just like array_diff_assoc
345
-	 * Order of keys/values does not matter
346
-	 *
347
-	 * @param array $array1
348
-	 * @param array $array2
349
-	 * @return array with the differences between $array1 and $array1
350
-	 * @throws \InvalidArgumentException if $array1 isn't an actual array
351
-	 *
352
-	 */
353
-	protected function array_diff_assoc_multi(array $array1, array $array2) {
354
-
355
-		$result = [];
356
-
357
-		foreach ($array1 as $key => $value) {
358
-
359
-			// if $array2 doesn't have the same key, that's a result
360
-			if (!array_key_exists($key, $array2)) {
361
-				$result[$key] = $value;
362
-				continue;
363
-			}
364
-
365
-			// if $array2's value for the same key is different, that's a result
366
-			if ($array2[$key] !== $value && !is_array($value)) {
367
-				$result[$key] = $value;
368
-				continue;
369
-			}
370
-
371
-			if (is_array($value)) {
372
-				$nestedDiff = $this->array_diff_assoc_multi($value, $array2[$key]);
373
-				if (!empty($nestedDiff)) {
374
-					$result[$key] = $nestedDiff;
375
-					continue;
376
-				}
377
-			}
378
-		}
379
-		return $result;
380
-	}
381
-
382
-	/**
383
-	 * Get the children currently in the cache
384
-	 *
385
-	 * @param int $folderId
386
-	 * @return array<string, \OCP\Files\Cache\ICacheEntry>
387
-	 */
388
-	protected function getExistingChildren($folderId): array {
389
-		$existingChildren = [];
390
-		$children = $this->cache->getFolderContentsById($folderId);
391
-		foreach ($children as $child) {
392
-			$existingChildren[$child['name']] = $child;
393
-		}
394
-		return $existingChildren;
395
-	}
396
-
397
-	/**
398
-	 * scan all the files and folders in a folder
399
-	 *
400
-	 * @param string $path
401
-	 * @param bool|IScanner::SCAN_RECURSIVE_INCOMPLETE $recursive
402
-	 * @param int $reuse a combination of self::REUSE_*
403
-	 * @param int $folderId id for the folder to be scanned
404
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
405
-	 * @param int|float $oldSize the size of the folder before (re)scanning the children
406
-	 * @return int|float the size of the scanned folder or -1 if the size is unknown at this stage
407
-	 */
408
-	protected function scanChildren(string $path, $recursive, int $reuse, int $folderId, bool $lock, int|float $oldSize, &$etagChanged = false) {
409
-		if ($reuse === -1) {
410
-			$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
411
-		}
412
-		$this->emit('\OC\Files\Cache\Scanner', 'scanFolder', [$path, $this->storageId]);
413
-		$size = 0;
414
-		$childQueue = $this->handleChildren($path, $recursive, $reuse, $folderId, $lock, $size, $etagChanged);
415
-
416
-		foreach ($childQueue as $child => [$childId, $childSize]) {
417
-			// "etag changed" propagates up, but not down, so we pass `false` to the children even if we already know that the etag of the current folder changed
418
-			$childEtagChanged = false;
419
-			$childSize = $this->scanChildren($child, $recursive, $reuse, $childId, $lock, $childSize, $childEtagChanged);
420
-			$etagChanged |= $childEtagChanged;
421
-
422
-			if ($childSize === -1) {
423
-				$size = -1;
424
-			} elseif ($size !== -1) {
425
-				$size += $childSize;
426
-			}
427
-		}
428
-
429
-		// for encrypted storages, we trigger a regular folder size calculation instead of using the calculated size
430
-		// to make sure we also updated the unencrypted-size where applicable
431
-		if ($this->storage->instanceOfStorage(Encryption::class)) {
432
-			$this->cache->calculateFolderSize($path);
433
-		} else {
434
-			if ($this->cacheActive) {
435
-				$updatedData = [];
436
-				if ($oldSize !== $size) {
437
-					$updatedData['size'] = $size;
438
-				}
439
-				if ($etagChanged) {
440
-					$updatedData['etag'] = uniqid();
441
-				}
442
-				if ($updatedData) {
443
-					$this->cache->update($folderId, $updatedData);
444
-				}
445
-			}
446
-		}
447
-		$this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', [$path, $this->storageId]);
448
-		return $size;
449
-	}
450
-
451
-	/**
452
-	 * @param bool|IScanner::SCAN_RECURSIVE_INCOMPLETE $recursive
453
-	 */
454
-	private function handleChildren(string $path, $recursive, int $reuse, int $folderId, bool $lock, int|float &$size, bool &$etagChanged): array {
455
-		// we put this in it's own function so it cleans up the memory before we start recursing
456
-		$existingChildren = $this->getExistingChildren($folderId);
457
-		$newChildren = iterator_to_array($this->storage->getDirectoryContent($path));
458
-
459
-		if (count($existingChildren) === 0 && count($newChildren) === 0) {
460
-			// no need to do a transaction
461
-			return [];
462
-		}
463
-
464
-		if ($this->useTransactions) {
465
-			$this->connection->beginTransaction();
466
-		}
467
-
468
-		$exceptionOccurred = false;
469
-		$childQueue = [];
470
-		$newChildNames = [];
471
-		foreach ($newChildren as $fileMeta) {
472
-			$permissions = $fileMeta['scan_permissions'] ?? $fileMeta['permissions'];
473
-			if ($permissions === 0) {
474
-				continue;
475
-			}
476
-			$originalFile = $fileMeta['name'];
477
-			$file = trim(\OC\Files\Filesystem::normalizePath($originalFile), '/');
478
-			if (trim($originalFile, '/') !== $file) {
479
-				// encoding mismatch, might require compatibility wrapper
480
-				\OC::$server->get(LoggerInterface::class)->debug('Scanner: Skipping non-normalized file name "' . $originalFile . '" in path "' . $path . '".', ['app' => 'core']);
481
-				$this->emit('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', [$path ? $path . '/' . $originalFile : $originalFile]);
482
-				// skip this entry
483
-				continue;
484
-			}
485
-
486
-			$newChildNames[] = $file;
487
-			$child = $path ? $path . '/' . $file : $file;
488
-			try {
489
-				$existingData = $existingChildren[$file] ?? false;
490
-				$data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock, $fileMeta);
491
-				if ($data) {
492
-					if ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE) {
493
-						$childQueue[$child] = [$data['fileid'], $data['size']];
494
-					} elseif ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE_INCOMPLETE && $data['size'] === -1) {
495
-						// only recurse into folders which aren't fully scanned
496
-						$childQueue[$child] = [$data['fileid'], $data['size']];
497
-					} elseif ($data['size'] === -1) {
498
-						$size = -1;
499
-					} elseif ($size !== -1) {
500
-						$size += $data['size'];
501
-					}
502
-
503
-					if (isset($data['etag_changed']) && $data['etag_changed']) {
504
-						$etagChanged = true;
505
-					}
506
-				}
507
-			} catch (Exception $ex) {
508
-				// might happen if inserting duplicate while a scanning
509
-				// process is running in parallel
510
-				// log and ignore
511
-				if ($this->useTransactions) {
512
-					$this->connection->rollback();
513
-					$this->connection->beginTransaction();
514
-				}
515
-				\OC::$server->get(LoggerInterface::class)->debug('Exception while scanning file "' . $child . '"', [
516
-					'app' => 'core',
517
-					'exception' => $ex,
518
-				]);
519
-				$exceptionOccurred = true;
520
-			} catch (\OCP\Lock\LockedException $e) {
521
-				if ($this->useTransactions) {
522
-					$this->connection->rollback();
523
-				}
524
-				throw $e;
525
-			}
526
-		}
527
-		$removedChildren = \array_diff(array_keys($existingChildren), $newChildNames);
528
-		foreach ($removedChildren as $childName) {
529
-			$child = $path ? $path . '/' . $childName : $childName;
530
-			$this->removeFromCache($child);
531
-		}
532
-		if ($this->useTransactions) {
533
-			$this->connection->commit();
534
-		}
535
-		if ($exceptionOccurred) {
536
-			// It might happen that the parallel scan process has already
537
-			// inserted mimetypes but those weren't available yet inside the transaction
538
-			// To make sure to have the updated mime types in such cases,
539
-			// we reload them here
540
-			\OC::$server->getMimeTypeLoader()->reset();
541
-		}
542
-		return $childQueue;
543
-	}
544
-
545
-	/**
546
-	 * check if the file should be ignored when scanning
547
-	 * NOTE: files with a '.part' extension are ignored as well!
548
-	 *       prevents unfinished put requests to be scanned
549
-	 *
550
-	 * @param string $file
551
-	 * @return boolean
552
-	 */
553
-	public static function isPartialFile($file) {
554
-		if (pathinfo($file, PATHINFO_EXTENSION) === 'part') {
555
-			return true;
556
-		}
557
-		if (str_contains($file, '.part/')) {
558
-			return true;
559
-		}
560
-
561
-		return false;
562
-	}
563
-
564
-	/**
565
-	 * walk over any folders that are not fully scanned yet and scan them
566
-	 */
567
-	public function backgroundScan() {
568
-		if ($this->storage->instanceOfStorage(Jail::class)) {
569
-			// for jail storage wrappers (shares, groupfolders) we run the background scan on the source storage
570
-			// this is mainly done because the jail wrapper doesn't implement `getIncomplete` (because it would be inefficient).
571
-			//
572
-			// Running the scan on the source storage might scan more than "needed", but the unscanned files outside the jail will
573
-			// have to be scanned at some point anyway.
574
-			$unJailedScanner = $this->storage->getUnjailedStorage()->getScanner();
575
-			$unJailedScanner->backgroundScan();
576
-		} else {
577
-			if (!$this->cache->inCache('')) {
578
-				// if the storage isn't in the cache yet, just scan the root completely
579
-				$this->runBackgroundScanJob(function () {
580
-					$this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
581
-				}, '');
582
-			} else {
583
-				$lastPath = null;
584
-				// find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
585
-				while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
586
-					$this->runBackgroundScanJob(function () use ($path) {
587
-						$this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
588
-					}, $path);
589
-					// FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
590
-					// to make this possible
591
-					$lastPath = $path;
592
-				}
593
-			}
594
-		}
595
-	}
596
-
597
-	protected function runBackgroundScanJob(callable $callback, $path) {
598
-		try {
599
-			$callback();
600
-			\OC_Hook::emit('Scanner', 'correctFolderSize', ['path' => $path]);
601
-			if ($this->cacheActive && $this->cache instanceof Cache) {
602
-				$this->cache->correctFolderSize($path, null, true);
603
-			}
604
-		} catch (\OCP\Files\StorageInvalidException $e) {
605
-			// skip unavailable storages
606
-		} catch (\OCP\Files\StorageNotAvailableException $e) {
607
-			// skip unavailable storages
608
-		} catch (\OCP\Files\ForbiddenException $e) {
609
-			// skip forbidden storages
610
-		} catch (\OCP\Lock\LockedException $e) {
611
-			// skip unavailable storages
612
-		}
613
-	}
614
-
615
-	/**
616
-	 * Set whether the cache is affected by scan operations
617
-	 *
618
-	 * @param boolean $active The active state of the cache
619
-	 */
620
-	public function setCacheActive($active) {
621
-		$this->cacheActive = $active;
622
-	}
36
+    /**
37
+     * @var \OC\Files\Storage\Storage $storage
38
+     */
39
+    protected $storage;
40
+
41
+    /**
42
+     * @var string $storageId
43
+     */
44
+    protected $storageId;
45
+
46
+    /**
47
+     * @var \OC\Files\Cache\Cache $cache
48
+     */
49
+    protected $cache;
50
+
51
+    /**
52
+     * @var boolean $cacheActive If true, perform cache operations, if false, do not affect cache
53
+     */
54
+    protected $cacheActive;
55
+
56
+    /**
57
+     * @var bool $useTransactions whether to use transactions
58
+     */
59
+    protected $useTransactions = true;
60
+
61
+    /**
62
+     * @var \OCP\Lock\ILockingProvider
63
+     */
64
+    protected $lockingProvider;
65
+
66
+    protected IDBConnection $connection;
67
+
68
+    public function __construct(\OC\Files\Storage\Storage $storage) {
69
+        $this->storage = $storage;
70
+        $this->storageId = $this->storage->getId();
71
+        $this->cache = $storage->getCache();
72
+        /** @var SystemConfig $config */
73
+        $config = \OC::$server->get(SystemConfig::class);
74
+        $this->cacheActive = !$config->getValue('filesystem_cache_readonly', false);
75
+        $this->useTransactions = !$config->getValue('filescanner_no_transactions', false);
76
+        $this->lockingProvider = \OC::$server->get(ILockingProvider::class);
77
+        $this->connection = \OC::$server->get(IDBConnection::class);
78
+    }
79
+
80
+    /**
81
+     * Whether to wrap the scanning of a folder in a database transaction
82
+     * On default transactions are used
83
+     *
84
+     * @param bool $useTransactions
85
+     */
86
+    public function setUseTransactions($useTransactions): void {
87
+        $this->useTransactions = $useTransactions;
88
+    }
89
+
90
+    /**
91
+     * get all the metadata of a file or folder
92
+     * *
93
+     *
94
+     * @param string $path
95
+     * @return array|null an array of metadata of the file
96
+     */
97
+    protected function getData($path) {
98
+        $data = $this->storage->getMetaData($path);
99
+        if (is_null($data)) {
100
+            \OC::$server->get(LoggerInterface::class)->debug("!!! Path '$path' is not accessible or present !!!", ['app' => 'core']);
101
+        }
102
+        return $data;
103
+    }
104
+
105
+    /**
106
+     * scan a single file and store it in the cache
107
+     *
108
+     * @param string $file
109
+     * @param int $reuseExisting
110
+     * @param int $parentId
111
+     * @param array|CacheEntry|null|false $cacheData existing data in the cache for the file to be scanned
112
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
113
+     * @param array|null $data the metadata for the file, as returned by the storage
114
+     * @return array|null an array of metadata of the scanned file
115
+     * @throws \OCP\Lock\LockedException
116
+     */
117
+    public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true, $data = null) {
118
+        if ($file !== '') {
119
+            try {
120
+                $this->storage->verifyPath(dirname($file), basename($file));
121
+            } catch (\Exception $e) {
122
+                return null;
123
+            }
124
+        }
125
+
126
+        // only proceed if $file is not a partial file, blacklist is handled by the storage
127
+        if (self::isPartialFile($file)) {
128
+            return null;
129
+        }
130
+
131
+        // acquire a lock
132
+        if ($lock) {
133
+            if ($this->storage->instanceOfStorage(ILockingStorage::class)) {
134
+                $this->storage->acquireLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
135
+            }
136
+        }
137
+
138
+        try {
139
+            $data = $data ?? $this->getData($file);
140
+        } catch (ForbiddenException $e) {
141
+            if ($lock) {
142
+                if ($this->storage->instanceOfStorage(ILockingStorage::class)) {
143
+                    $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
144
+                }
145
+            }
146
+
147
+            return null;
148
+        }
149
+
150
+        try {
151
+            if ($data === null) {
152
+                $this->removeFromCache($file);
153
+            } else {
154
+                // pre-emit only if it was a file. By that we avoid counting/treating folders as files
155
+                if ($data['mimetype'] !== 'httpd/unix-directory') {
156
+                    $this->emit('\OC\Files\Cache\Scanner', 'scanFile', [$file, $this->storageId]);
157
+                    \OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', ['path' => $file, 'storage' => $this->storageId]);
158
+                }
159
+
160
+                $parent = dirname($file);
161
+                if ($parent === '.' || $parent === '/') {
162
+                    $parent = '';
163
+                }
164
+                if ($parentId === -1) {
165
+                    $parentId = $this->cache->getParentId($file);
166
+                }
167
+
168
+                // scan the parent if it's not in the cache (id -1) and the current file is not the root folder
169
+                if ($file && $parentId === -1) {
170
+                    $parentData = $this->scanFile($parent);
171
+                    if ($parentData === null) {
172
+                        return null;
173
+                    }
174
+
175
+                    $parentId = $parentData['fileid'];
176
+                }
177
+                if ($parent) {
178
+                    $data['parent'] = $parentId;
179
+                }
180
+
181
+                $cacheData = $cacheData ?? $this->cache->get($file);
182
+                if ($reuseExisting && $cacheData !== false && isset($cacheData['fileid'])) {
183
+                    // prevent empty etag
184
+                    $etag = empty($cacheData['etag']) ? $data['etag'] : $cacheData['etag'];
185
+                    $fileId = $cacheData['fileid'];
186
+                    $data['fileid'] = $fileId;
187
+                    // only reuse data if the file hasn't explicitly changed
188
+                    $mtimeUnchanged = isset($data['storage_mtime']) && isset($cacheData['storage_mtime']) && $data['storage_mtime'] === $cacheData['storage_mtime'];
189
+                    // if the folder is marked as unscanned, never reuse etags
190
+                    if ($mtimeUnchanged && $cacheData['size'] !== -1) {
191
+                        $data['mtime'] = $cacheData['mtime'];
192
+                        if (($reuseExisting & self::REUSE_SIZE) && ($data['size'] === -1)) {
193
+                            $data['size'] = $cacheData['size'];
194
+                        }
195
+                        if ($reuseExisting & self::REUSE_ETAG && !$this->storage->instanceOfStorage(IReliableEtagStorage::class)) {
196
+                            $data['etag'] = $etag;
197
+                        }
198
+                    }
199
+
200
+                    // we only updated unencrypted_size if it's already set
201
+                    if (isset($cacheData['unencrypted_size']) && $cacheData['unencrypted_size'] === 0) {
202
+                        unset($data['unencrypted_size']);
203
+                    }
204
+
205
+                    /**
206
+                     * Only update metadata that has changed.
207
+                     * i.e. get all the values in $data that are not present in the cache already
208
+                     *
209
+                     * We need the OC implementation for usage of "getData" method below.
210
+                     * @var \OC\Files\Cache\CacheEntry $cacheData
211
+                     */
212
+                    $newData = $this->array_diff_assoc_multi($data, $cacheData->getData());
213
+
214
+                    // make it known to the caller that etag has been changed and needs propagation
215
+                    if (isset($newData['etag'])) {
216
+                        $data['etag_changed'] = true;
217
+                    }
218
+                } else {
219
+                    unset($data['unencrypted_size']);
220
+                    $newData = $data;
221
+                    $fileId = -1;
222
+                }
223
+                if (!empty($newData)) {
224
+                    // Reset the checksum if the data has changed
225
+                    $newData['checksum'] = '';
226
+                    $newData['parent'] = $parentId;
227
+                    $data['fileid'] = $this->addToCache($file, $newData, $fileId);
228
+                }
229
+
230
+                if ($cacheData !== false) {
231
+                    $data['oldSize'] = $cacheData['size'] ?? 0;
232
+                    $data['encrypted'] = $cacheData['encrypted'] ?? false;
233
+                }
234
+
235
+                // post-emit only if it was a file. By that we avoid counting/treating folders as files
236
+                if ($data['mimetype'] !== 'httpd/unix-directory') {
237
+                    $this->emit('\OC\Files\Cache\Scanner', 'postScanFile', [$file, $this->storageId]);
238
+                    \OC_Hook::emit('\OC\Files\Cache\Scanner', 'post_scan_file', ['path' => $file, 'storage' => $this->storageId]);
239
+                }
240
+            }
241
+        } finally {
242
+            // release the acquired lock
243
+            if ($lock && $this->storage->instanceOfStorage(ILockingStorage::class)) {
244
+                $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
245
+            }
246
+        }
247
+
248
+        return $data;
249
+    }
250
+
251
+    protected function removeFromCache($path) {
252
+        \OC_Hook::emit('Scanner', 'removeFromCache', ['file' => $path]);
253
+        $this->emit('\OC\Files\Cache\Scanner', 'removeFromCache', [$path]);
254
+        if ($this->cacheActive) {
255
+            $this->cache->remove($path);
256
+        }
257
+    }
258
+
259
+    /**
260
+     * @param string $path
261
+     * @param array $data
262
+     * @param int $fileId
263
+     * @return int the id of the added file
264
+     */
265
+    protected function addToCache($path, $data, $fileId = -1) {
266
+        if (isset($data['scan_permissions'])) {
267
+            $data['permissions'] = $data['scan_permissions'];
268
+        }
269
+        \OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
270
+        $this->emit('\OC\Files\Cache\Scanner', 'addToCache', [$path, $this->storageId, $data, $fileId]);
271
+        if ($this->cacheActive) {
272
+            if ($fileId !== -1) {
273
+                $this->cache->update($fileId, $data);
274
+                return $fileId;
275
+            } else {
276
+                return $this->cache->insert($path, $data);
277
+            }
278
+        } else {
279
+            return -1;
280
+        }
281
+    }
282
+
283
+    /**
284
+     * @param string $path
285
+     * @param array $data
286
+     * @param int $fileId
287
+     */
288
+    protected function updateCache($path, $data, $fileId = -1) {
289
+        \OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
290
+        $this->emit('\OC\Files\Cache\Scanner', 'updateCache', [$path, $this->storageId, $data]);
291
+        if ($this->cacheActive) {
292
+            if ($fileId !== -1) {
293
+                $this->cache->update($fileId, $data);
294
+            } else {
295
+                $this->cache->put($path, $data);
296
+            }
297
+        }
298
+    }
299
+
300
+    /**
301
+     * scan a folder and all it's children
302
+     *
303
+     * @param string $path
304
+     * @param bool $recursive
305
+     * @param int $reuse
306
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
307
+     * @return array|null an array of the meta data of the scanned file or folder
308
+     */
309
+    public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
310
+        if ($reuse === -1) {
311
+            $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
312
+        }
313
+
314
+        if ($lock && $this->storage->instanceOfStorage(ILockingStorage::class)) {
315
+            $this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
316
+            $this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
317
+        }
318
+
319
+        try {
320
+            $data = $this->scanFile($path, $reuse, -1, lock: $lock);
321
+
322
+            if ($data !== null && $data['mimetype'] === 'httpd/unix-directory') {
323
+                $size = $this->scanChildren($path, $recursive, $reuse, $data['fileid'], $lock, $data['size']);
324
+                $data['size'] = $size;
325
+            }
326
+        } catch (NotFoundException $e) {
327
+            $this->removeFromCache($path);
328
+            return null;
329
+        } finally {
330
+            if ($lock && $this->storage->instanceOfStorage(ILockingStorage::class)) {
331
+                $this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
332
+                $this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
333
+            }
334
+        }
335
+        return $data;
336
+    }
337
+
338
+    /**
339
+     * Compares $array1 against $array2 and returns all the values in $array1 that are not in $array2
340
+     * Note this is a one-way check - i.e. we don't care about things that are in $array2 that aren't in $array1
341
+     *
342
+     * Supports multi-dimensional arrays
343
+     * Also checks keys/indexes
344
+     * Comparisons are strict just like array_diff_assoc
345
+     * Order of keys/values does not matter
346
+     *
347
+     * @param array $array1
348
+     * @param array $array2
349
+     * @return array with the differences between $array1 and $array1
350
+     * @throws \InvalidArgumentException if $array1 isn't an actual array
351
+     *
352
+     */
353
+    protected function array_diff_assoc_multi(array $array1, array $array2) {
354
+
355
+        $result = [];
356
+
357
+        foreach ($array1 as $key => $value) {
358
+
359
+            // if $array2 doesn't have the same key, that's a result
360
+            if (!array_key_exists($key, $array2)) {
361
+                $result[$key] = $value;
362
+                continue;
363
+            }
364
+
365
+            // if $array2's value for the same key is different, that's a result
366
+            if ($array2[$key] !== $value && !is_array($value)) {
367
+                $result[$key] = $value;
368
+                continue;
369
+            }
370
+
371
+            if (is_array($value)) {
372
+                $nestedDiff = $this->array_diff_assoc_multi($value, $array2[$key]);
373
+                if (!empty($nestedDiff)) {
374
+                    $result[$key] = $nestedDiff;
375
+                    continue;
376
+                }
377
+            }
378
+        }
379
+        return $result;
380
+    }
381
+
382
+    /**
383
+     * Get the children currently in the cache
384
+     *
385
+     * @param int $folderId
386
+     * @return array<string, \OCP\Files\Cache\ICacheEntry>
387
+     */
388
+    protected function getExistingChildren($folderId): array {
389
+        $existingChildren = [];
390
+        $children = $this->cache->getFolderContentsById($folderId);
391
+        foreach ($children as $child) {
392
+            $existingChildren[$child['name']] = $child;
393
+        }
394
+        return $existingChildren;
395
+    }
396
+
397
+    /**
398
+     * scan all the files and folders in a folder
399
+     *
400
+     * @param string $path
401
+     * @param bool|IScanner::SCAN_RECURSIVE_INCOMPLETE $recursive
402
+     * @param int $reuse a combination of self::REUSE_*
403
+     * @param int $folderId id for the folder to be scanned
404
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
405
+     * @param int|float $oldSize the size of the folder before (re)scanning the children
406
+     * @return int|float the size of the scanned folder or -1 if the size is unknown at this stage
407
+     */
408
+    protected function scanChildren(string $path, $recursive, int $reuse, int $folderId, bool $lock, int|float $oldSize, &$etagChanged = false) {
409
+        if ($reuse === -1) {
410
+            $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
411
+        }
412
+        $this->emit('\OC\Files\Cache\Scanner', 'scanFolder', [$path, $this->storageId]);
413
+        $size = 0;
414
+        $childQueue = $this->handleChildren($path, $recursive, $reuse, $folderId, $lock, $size, $etagChanged);
415
+
416
+        foreach ($childQueue as $child => [$childId, $childSize]) {
417
+            // "etag changed" propagates up, but not down, so we pass `false` to the children even if we already know that the etag of the current folder changed
418
+            $childEtagChanged = false;
419
+            $childSize = $this->scanChildren($child, $recursive, $reuse, $childId, $lock, $childSize, $childEtagChanged);
420
+            $etagChanged |= $childEtagChanged;
421
+
422
+            if ($childSize === -1) {
423
+                $size = -1;
424
+            } elseif ($size !== -1) {
425
+                $size += $childSize;
426
+            }
427
+        }
428
+
429
+        // for encrypted storages, we trigger a regular folder size calculation instead of using the calculated size
430
+        // to make sure we also updated the unencrypted-size where applicable
431
+        if ($this->storage->instanceOfStorage(Encryption::class)) {
432
+            $this->cache->calculateFolderSize($path);
433
+        } else {
434
+            if ($this->cacheActive) {
435
+                $updatedData = [];
436
+                if ($oldSize !== $size) {
437
+                    $updatedData['size'] = $size;
438
+                }
439
+                if ($etagChanged) {
440
+                    $updatedData['etag'] = uniqid();
441
+                }
442
+                if ($updatedData) {
443
+                    $this->cache->update($folderId, $updatedData);
444
+                }
445
+            }
446
+        }
447
+        $this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', [$path, $this->storageId]);
448
+        return $size;
449
+    }
450
+
451
+    /**
452
+     * @param bool|IScanner::SCAN_RECURSIVE_INCOMPLETE $recursive
453
+     */
454
+    private function handleChildren(string $path, $recursive, int $reuse, int $folderId, bool $lock, int|float &$size, bool &$etagChanged): array {
455
+        // we put this in it's own function so it cleans up the memory before we start recursing
456
+        $existingChildren = $this->getExistingChildren($folderId);
457
+        $newChildren = iterator_to_array($this->storage->getDirectoryContent($path));
458
+
459
+        if (count($existingChildren) === 0 && count($newChildren) === 0) {
460
+            // no need to do a transaction
461
+            return [];
462
+        }
463
+
464
+        if ($this->useTransactions) {
465
+            $this->connection->beginTransaction();
466
+        }
467
+
468
+        $exceptionOccurred = false;
469
+        $childQueue = [];
470
+        $newChildNames = [];
471
+        foreach ($newChildren as $fileMeta) {
472
+            $permissions = $fileMeta['scan_permissions'] ?? $fileMeta['permissions'];
473
+            if ($permissions === 0) {
474
+                continue;
475
+            }
476
+            $originalFile = $fileMeta['name'];
477
+            $file = trim(\OC\Files\Filesystem::normalizePath($originalFile), '/');
478
+            if (trim($originalFile, '/') !== $file) {
479
+                // encoding mismatch, might require compatibility wrapper
480
+                \OC::$server->get(LoggerInterface::class)->debug('Scanner: Skipping non-normalized file name "' . $originalFile . '" in path "' . $path . '".', ['app' => 'core']);
481
+                $this->emit('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', [$path ? $path . '/' . $originalFile : $originalFile]);
482
+                // skip this entry
483
+                continue;
484
+            }
485
+
486
+            $newChildNames[] = $file;
487
+            $child = $path ? $path . '/' . $file : $file;
488
+            try {
489
+                $existingData = $existingChildren[$file] ?? false;
490
+                $data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock, $fileMeta);
491
+                if ($data) {
492
+                    if ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE) {
493
+                        $childQueue[$child] = [$data['fileid'], $data['size']];
494
+                    } elseif ($data['mimetype'] === 'httpd/unix-directory' && $recursive === self::SCAN_RECURSIVE_INCOMPLETE && $data['size'] === -1) {
495
+                        // only recurse into folders which aren't fully scanned
496
+                        $childQueue[$child] = [$data['fileid'], $data['size']];
497
+                    } elseif ($data['size'] === -1) {
498
+                        $size = -1;
499
+                    } elseif ($size !== -1) {
500
+                        $size += $data['size'];
501
+                    }
502
+
503
+                    if (isset($data['etag_changed']) && $data['etag_changed']) {
504
+                        $etagChanged = true;
505
+                    }
506
+                }
507
+            } catch (Exception $ex) {
508
+                // might happen if inserting duplicate while a scanning
509
+                // process is running in parallel
510
+                // log and ignore
511
+                if ($this->useTransactions) {
512
+                    $this->connection->rollback();
513
+                    $this->connection->beginTransaction();
514
+                }
515
+                \OC::$server->get(LoggerInterface::class)->debug('Exception while scanning file "' . $child . '"', [
516
+                    'app' => 'core',
517
+                    'exception' => $ex,
518
+                ]);
519
+                $exceptionOccurred = true;
520
+            } catch (\OCP\Lock\LockedException $e) {
521
+                if ($this->useTransactions) {
522
+                    $this->connection->rollback();
523
+                }
524
+                throw $e;
525
+            }
526
+        }
527
+        $removedChildren = \array_diff(array_keys($existingChildren), $newChildNames);
528
+        foreach ($removedChildren as $childName) {
529
+            $child = $path ? $path . '/' . $childName : $childName;
530
+            $this->removeFromCache($child);
531
+        }
532
+        if ($this->useTransactions) {
533
+            $this->connection->commit();
534
+        }
535
+        if ($exceptionOccurred) {
536
+            // It might happen that the parallel scan process has already
537
+            // inserted mimetypes but those weren't available yet inside the transaction
538
+            // To make sure to have the updated mime types in such cases,
539
+            // we reload them here
540
+            \OC::$server->getMimeTypeLoader()->reset();
541
+        }
542
+        return $childQueue;
543
+    }
544
+
545
+    /**
546
+     * check if the file should be ignored when scanning
547
+     * NOTE: files with a '.part' extension are ignored as well!
548
+     *       prevents unfinished put requests to be scanned
549
+     *
550
+     * @param string $file
551
+     * @return boolean
552
+     */
553
+    public static function isPartialFile($file) {
554
+        if (pathinfo($file, PATHINFO_EXTENSION) === 'part') {
555
+            return true;
556
+        }
557
+        if (str_contains($file, '.part/')) {
558
+            return true;
559
+        }
560
+
561
+        return false;
562
+    }
563
+
564
+    /**
565
+     * walk over any folders that are not fully scanned yet and scan them
566
+     */
567
+    public function backgroundScan() {
568
+        if ($this->storage->instanceOfStorage(Jail::class)) {
569
+            // for jail storage wrappers (shares, groupfolders) we run the background scan on the source storage
570
+            // this is mainly done because the jail wrapper doesn't implement `getIncomplete` (because it would be inefficient).
571
+            //
572
+            // Running the scan on the source storage might scan more than "needed", but the unscanned files outside the jail will
573
+            // have to be scanned at some point anyway.
574
+            $unJailedScanner = $this->storage->getUnjailedStorage()->getScanner();
575
+            $unJailedScanner->backgroundScan();
576
+        } else {
577
+            if (!$this->cache->inCache('')) {
578
+                // if the storage isn't in the cache yet, just scan the root completely
579
+                $this->runBackgroundScanJob(function () {
580
+                    $this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
581
+                }, '');
582
+            } else {
583
+                $lastPath = null;
584
+                // find any path marked as unscanned and run the scanner until no more paths are unscanned (or we get stuck)
585
+                while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
586
+                    $this->runBackgroundScanJob(function () use ($path) {
587
+                        $this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
588
+                    }, $path);
589
+                    // FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
590
+                    // to make this possible
591
+                    $lastPath = $path;
592
+                }
593
+            }
594
+        }
595
+    }
596
+
597
+    protected function runBackgroundScanJob(callable $callback, $path) {
598
+        try {
599
+            $callback();
600
+            \OC_Hook::emit('Scanner', 'correctFolderSize', ['path' => $path]);
601
+            if ($this->cacheActive && $this->cache instanceof Cache) {
602
+                $this->cache->correctFolderSize($path, null, true);
603
+            }
604
+        } catch (\OCP\Files\StorageInvalidException $e) {
605
+            // skip unavailable storages
606
+        } catch (\OCP\Files\StorageNotAvailableException $e) {
607
+            // skip unavailable storages
608
+        } catch (\OCP\Files\ForbiddenException $e) {
609
+            // skip forbidden storages
610
+        } catch (\OCP\Lock\LockedException $e) {
611
+            // skip unavailable storages
612
+        }
613
+    }
614
+
615
+    /**
616
+     * Set whether the cache is affected by scan operations
617
+     *
618
+     * @param boolean $active The active state of the cache
619
+     */
620
+    public function setCacheActive($active) {
621
+        $this->cacheActive = $active;
622
+    }
623 623
 }
Please login to merge, or discard this patch.
lib/private/Files/Utils/Scanner.php 1 patch
Indentation   +237 added lines, -237 removed lines patch added patch discarded remove patch
@@ -42,266 +42,266 @@
 block discarded – undo
42 42
  * @package OC\Files\Utils
43 43
  */
44 44
 class Scanner extends PublicEmitter {
45
-	public const MAX_ENTRIES_TO_COMMIT = 10000;
45
+    public const MAX_ENTRIES_TO_COMMIT = 10000;
46 46
 
47
-	/** @var string $user */
48
-	private $user;
47
+    /** @var string $user */
48
+    private $user;
49 49
 
50
-	/** @var IDBConnection */
51
-	protected $db;
50
+    /** @var IDBConnection */
51
+    protected $db;
52 52
 
53
-	/** @var IEventDispatcher */
54
-	private $dispatcher;
53
+    /** @var IEventDispatcher */
54
+    private $dispatcher;
55 55
 
56
-	protected LoggerInterface $logger;
56
+    protected LoggerInterface $logger;
57 57
 
58
-	/**
59
-	 * Whether to use a DB transaction
60
-	 *
61
-	 * @var bool
62
-	 */
63
-	protected $useTransaction;
58
+    /**
59
+     * Whether to use a DB transaction
60
+     *
61
+     * @var bool
62
+     */
63
+    protected $useTransaction;
64 64
 
65
-	/**
66
-	 * Number of entries scanned to commit
67
-	 *
68
-	 * @var int
69
-	 */
70
-	protected $entriesToCommit;
65
+    /**
66
+     * Number of entries scanned to commit
67
+     *
68
+     * @var int
69
+     */
70
+    protected $entriesToCommit;
71 71
 
72
-	/**
73
-	 * @param string $user
74
-	 * @param IDBConnection|null $db
75
-	 * @param IEventDispatcher $dispatcher
76
-	 */
77
-	public function __construct($user, $db, IEventDispatcher $dispatcher, LoggerInterface $logger) {
78
-		$this->user = $user;
79
-		$this->db = $db;
80
-		$this->dispatcher = $dispatcher;
81
-		$this->logger = $logger;
82
-		// when DB locking is used, no DB transactions will be used
83
-		$this->useTransaction = !(\OC::$server->get(ILockingProvider::class) instanceof DBLockingProvider);
84
-	}
72
+    /**
73
+     * @param string $user
74
+     * @param IDBConnection|null $db
75
+     * @param IEventDispatcher $dispatcher
76
+     */
77
+    public function __construct($user, $db, IEventDispatcher $dispatcher, LoggerInterface $logger) {
78
+        $this->user = $user;
79
+        $this->db = $db;
80
+        $this->dispatcher = $dispatcher;
81
+        $this->logger = $logger;
82
+        // when DB locking is used, no DB transactions will be used
83
+        $this->useTransaction = !(\OC::$server->get(ILockingProvider::class) instanceof DBLockingProvider);
84
+    }
85 85
 
86
-	/**
87
-	 * get all storages for $dir
88
-	 *
89
-	 * @param string $dir
90
-	 * @return array<string, IMountPoint>
91
-	 */
92
-	protected function getMounts($dir) {
93
-		//TODO: move to the node based fileapi once that's done
94
-		\OC_Util::tearDownFS();
95
-		\OC_Util::setupFS($this->user);
86
+    /**
87
+     * get all storages for $dir
88
+     *
89
+     * @param string $dir
90
+     * @return array<string, IMountPoint>
91
+     */
92
+    protected function getMounts($dir) {
93
+        //TODO: move to the node based fileapi once that's done
94
+        \OC_Util::tearDownFS();
95
+        \OC_Util::setupFS($this->user);
96 96
 
97
-		$mountManager = Filesystem::getMountManager();
98
-		$mounts = $mountManager->findIn($dir);
99
-		$mounts[] = $mountManager->find($dir);
100
-		$mounts = array_reverse($mounts); //start with the mount of $dir
101
-		$mountPoints = array_map(fn ($mount) => $mount->getMountPoint(), $mounts);
97
+        $mountManager = Filesystem::getMountManager();
98
+        $mounts = $mountManager->findIn($dir);
99
+        $mounts[] = $mountManager->find($dir);
100
+        $mounts = array_reverse($mounts); //start with the mount of $dir
101
+        $mountPoints = array_map(fn ($mount) => $mount->getMountPoint(), $mounts);
102 102
 
103
-		return array_combine($mountPoints, $mounts);
104
-	}
103
+        return array_combine($mountPoints, $mounts);
104
+    }
105 105
 
106
-	/**
107
-	 * attach listeners to the scanner
108
-	 *
109
-	 * @param \OC\Files\Mount\MountPoint $mount
110
-	 */
111
-	protected function attachListener($mount) {
112
-		/** @var \OC\Files\Cache\Scanner $scanner */
113
-		$scanner = $mount->getStorage()->getScanner();
114
-		$scanner->listen('\OC\Files\Cache\Scanner', 'scanFile', function ($path) use ($mount) {
115
-			$this->emit('\OC\Files\Utils\Scanner', 'scanFile', [$mount->getMountPoint() . $path]);
116
-			$this->dispatcher->dispatchTyped(new BeforeFileScannedEvent($mount->getMountPoint() . $path));
117
-		});
118
-		$scanner->listen('\OC\Files\Cache\Scanner', 'scanFolder', function ($path) use ($mount) {
119
-			$this->emit('\OC\Files\Utils\Scanner', 'scanFolder', [$mount->getMountPoint() . $path]);
120
-			$this->dispatcher->dispatchTyped(new BeforeFolderScannedEvent($mount->getMountPoint() . $path));
121
-		});
122
-		$scanner->listen('\OC\Files\Cache\Scanner', 'postScanFile', function ($path) use ($mount) {
123
-			$this->emit('\OC\Files\Utils\Scanner', 'postScanFile', [$mount->getMountPoint() . $path]);
124
-			$this->dispatcher->dispatchTyped(new FileScannedEvent($mount->getMountPoint() . $path));
125
-		});
126
-		$scanner->listen('\OC\Files\Cache\Scanner', 'postScanFolder', function ($path) use ($mount) {
127
-			$this->emit('\OC\Files\Utils\Scanner', 'postScanFolder', [$mount->getMountPoint() . $path]);
128
-			$this->dispatcher->dispatchTyped(new FolderScannedEvent($mount->getMountPoint() . $path));
129
-		});
130
-		$scanner->listen('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', function ($path) use ($mount) {
131
-			$this->emit('\OC\Files\Utils\Scanner', 'normalizedNameMismatch', [$path]);
132
-		});
133
-	}
106
+    /**
107
+     * attach listeners to the scanner
108
+     *
109
+     * @param \OC\Files\Mount\MountPoint $mount
110
+     */
111
+    protected function attachListener($mount) {
112
+        /** @var \OC\Files\Cache\Scanner $scanner */
113
+        $scanner = $mount->getStorage()->getScanner();
114
+        $scanner->listen('\OC\Files\Cache\Scanner', 'scanFile', function ($path) use ($mount) {
115
+            $this->emit('\OC\Files\Utils\Scanner', 'scanFile', [$mount->getMountPoint() . $path]);
116
+            $this->dispatcher->dispatchTyped(new BeforeFileScannedEvent($mount->getMountPoint() . $path));
117
+        });
118
+        $scanner->listen('\OC\Files\Cache\Scanner', 'scanFolder', function ($path) use ($mount) {
119
+            $this->emit('\OC\Files\Utils\Scanner', 'scanFolder', [$mount->getMountPoint() . $path]);
120
+            $this->dispatcher->dispatchTyped(new BeforeFolderScannedEvent($mount->getMountPoint() . $path));
121
+        });
122
+        $scanner->listen('\OC\Files\Cache\Scanner', 'postScanFile', function ($path) use ($mount) {
123
+            $this->emit('\OC\Files\Utils\Scanner', 'postScanFile', [$mount->getMountPoint() . $path]);
124
+            $this->dispatcher->dispatchTyped(new FileScannedEvent($mount->getMountPoint() . $path));
125
+        });
126
+        $scanner->listen('\OC\Files\Cache\Scanner', 'postScanFolder', function ($path) use ($mount) {
127
+            $this->emit('\OC\Files\Utils\Scanner', 'postScanFolder', [$mount->getMountPoint() . $path]);
128
+            $this->dispatcher->dispatchTyped(new FolderScannedEvent($mount->getMountPoint() . $path));
129
+        });
130
+        $scanner->listen('\OC\Files\Cache\Scanner', 'normalizedNameMismatch', function ($path) use ($mount) {
131
+            $this->emit('\OC\Files\Utils\Scanner', 'normalizedNameMismatch', [$path]);
132
+        });
133
+    }
134 134
 
135
-	/**
136
-	 * @param string $dir
137
-	 */
138
-	public function backgroundScan($dir) {
139
-		$mounts = $this->getMounts($dir);
140
-		foreach ($mounts as $mount) {
141
-			try {
142
-				$storage = $mount->getStorage();
143
-				if (is_null($storage)) {
144
-					continue;
145
-				}
135
+    /**
136
+     * @param string $dir
137
+     */
138
+    public function backgroundScan($dir) {
139
+        $mounts = $this->getMounts($dir);
140
+        foreach ($mounts as $mount) {
141
+            try {
142
+                $storage = $mount->getStorage();
143
+                if (is_null($storage)) {
144
+                    continue;
145
+                }
146 146
 
147
-				// don't bother scanning failed storages (shortcut for same result)
148
-				if ($storage->instanceOfStorage(FailedStorage::class)) {
149
-					continue;
150
-				}
147
+                // don't bother scanning failed storages (shortcut for same result)
148
+                if ($storage->instanceOfStorage(FailedStorage::class)) {
149
+                    continue;
150
+                }
151 151
 
152
-				/** @var \OC\Files\Cache\Scanner $scanner */
153
-				$scanner = $storage->getScanner();
154
-				$this->attachListener($mount);
152
+                /** @var \OC\Files\Cache\Scanner $scanner */
153
+                $scanner = $storage->getScanner();
154
+                $this->attachListener($mount);
155 155
 
156
-				$scanner->listen('\OC\Files\Cache\Scanner', 'removeFromCache', function ($path) use ($storage) {
157
-					$this->triggerPropagator($storage, $path);
158
-				});
159
-				$scanner->listen('\OC\Files\Cache\Scanner', 'updateCache', function ($path) use ($storage) {
160
-					$this->triggerPropagator($storage, $path);
161
-				});
162
-				$scanner->listen('\OC\Files\Cache\Scanner', 'addToCache', function ($path) use ($storage) {
163
-					$this->triggerPropagator($storage, $path);
164
-				});
156
+                $scanner->listen('\OC\Files\Cache\Scanner', 'removeFromCache', function ($path) use ($storage) {
157
+                    $this->triggerPropagator($storage, $path);
158
+                });
159
+                $scanner->listen('\OC\Files\Cache\Scanner', 'updateCache', function ($path) use ($storage) {
160
+                    $this->triggerPropagator($storage, $path);
161
+                });
162
+                $scanner->listen('\OC\Files\Cache\Scanner', 'addToCache', function ($path) use ($storage) {
163
+                    $this->triggerPropagator($storage, $path);
164
+                });
165 165
 
166
-				$propagator = $storage->getPropagator();
167
-				$propagator->beginBatch();
168
-				$scanner->backgroundScan();
169
-				$propagator->commitBatch();
170
-			} catch (\Exception $e) {
171
-				$this->logger->error("Error while trying to scan mount as {$mount->getMountPoint()}:" . $e->getMessage(), ['exception' => $e, 'app' => 'files']);
172
-			}
173
-		}
174
-	}
166
+                $propagator = $storage->getPropagator();
167
+                $propagator->beginBatch();
168
+                $scanner->backgroundScan();
169
+                $propagator->commitBatch();
170
+            } catch (\Exception $e) {
171
+                $this->logger->error("Error while trying to scan mount as {$mount->getMountPoint()}:" . $e->getMessage(), ['exception' => $e, 'app' => 'files']);
172
+            }
173
+        }
174
+    }
175 175
 
176
-	/**
177
-	 * @param string $dir
178
-	 * @param $recursive
179
-	 * @param callable|null $mountFilter
180
-	 * @throws ForbiddenException
181
-	 * @throws NotFoundException
182
-	 */
183
-	public function scan($dir = '', $recursive = \OC\Files\Cache\Scanner::SCAN_RECURSIVE, ?callable $mountFilter = null) {
184
-		if (!Filesystem::isValidPath($dir)) {
185
-			throw new \InvalidArgumentException('Invalid path to scan');
186
-		}
187
-		$mounts = $this->getMounts($dir);
188
-		foreach ($mounts as $mount) {
189
-			if ($mountFilter && !$mountFilter($mount)) {
190
-				continue;
191
-			}
192
-			$storage = $mount->getStorage();
193
-			if (is_null($storage)) {
194
-				continue;
195
-			}
176
+    /**
177
+     * @param string $dir
178
+     * @param $recursive
179
+     * @param callable|null $mountFilter
180
+     * @throws ForbiddenException
181
+     * @throws NotFoundException
182
+     */
183
+    public function scan($dir = '', $recursive = \OC\Files\Cache\Scanner::SCAN_RECURSIVE, ?callable $mountFilter = null) {
184
+        if (!Filesystem::isValidPath($dir)) {
185
+            throw new \InvalidArgumentException('Invalid path to scan');
186
+        }
187
+        $mounts = $this->getMounts($dir);
188
+        foreach ($mounts as $mount) {
189
+            if ($mountFilter && !$mountFilter($mount)) {
190
+                continue;
191
+            }
192
+            $storage = $mount->getStorage();
193
+            if (is_null($storage)) {
194
+                continue;
195
+            }
196 196
 
197
-			// don't bother scanning failed storages (shortcut for same result)
198
-			if ($storage->instanceOfStorage(FailedStorage::class)) {
199
-				continue;
200
-			}
197
+            // don't bother scanning failed storages (shortcut for same result)
198
+            if ($storage->instanceOfStorage(FailedStorage::class)) {
199
+                continue;
200
+            }
201 201
 
202
-			// if the home storage isn't writable then the scanner is run as the wrong user
203
-			if ($storage->instanceOfStorage(Home::class)) {
204
-				/** @var Home $storage */
205
-				foreach (['', 'files'] as $path) {
206
-					if (!$storage->isCreatable($path)) {
207
-						$fullPath = $storage->getSourcePath($path);
208
-						if (!$storage->is_dir($path) && $storage->getCache()->inCache($path)) {
209
-							throw new NotFoundException("User folder $fullPath exists in cache but not on disk");
210
-						} elseif ($storage->is_dir($path)) {
211
-							$ownerUid = fileowner($fullPath);
212
-							$owner = posix_getpwuid($ownerUid);
213
-							$owner = $owner['name'] ?? $ownerUid;
214
-							$permissions = decoct(fileperms($fullPath));
215
-							throw new ForbiddenException("User folder $fullPath is not writable, folders is owned by $owner and has mode $permissions");
216
-						} elseif (isset($mounts[$mount->getMountPoint() . $path . '/'])) {
217
-							// /<user>/files is overwritten by a mountpoint, so this check is irrelevant
218
-							break;
219
-						} else {
220
-							// if the root exists in neither the cache nor the storage the user isn't setup yet
221
-							break 2;
222
-						}
223
-					}
224
-				}
225
-			}
202
+            // if the home storage isn't writable then the scanner is run as the wrong user
203
+            if ($storage->instanceOfStorage(Home::class)) {
204
+                /** @var Home $storage */
205
+                foreach (['', 'files'] as $path) {
206
+                    if (!$storage->isCreatable($path)) {
207
+                        $fullPath = $storage->getSourcePath($path);
208
+                        if (!$storage->is_dir($path) && $storage->getCache()->inCache($path)) {
209
+                            throw new NotFoundException("User folder $fullPath exists in cache but not on disk");
210
+                        } elseif ($storage->is_dir($path)) {
211
+                            $ownerUid = fileowner($fullPath);
212
+                            $owner = posix_getpwuid($ownerUid);
213
+                            $owner = $owner['name'] ?? $ownerUid;
214
+                            $permissions = decoct(fileperms($fullPath));
215
+                            throw new ForbiddenException("User folder $fullPath is not writable, folders is owned by $owner and has mode $permissions");
216
+                        } elseif (isset($mounts[$mount->getMountPoint() . $path . '/'])) {
217
+                            // /<user>/files is overwritten by a mountpoint, so this check is irrelevant
218
+                            break;
219
+                        } else {
220
+                            // if the root exists in neither the cache nor the storage the user isn't setup yet
221
+                            break 2;
222
+                        }
223
+                    }
224
+                }
225
+            }
226 226
 
227
-			// don't scan received local shares, these can be scanned when scanning the owner's storage
228
-			if ($storage->instanceOfStorage(SharedStorage::class)) {
229
-				continue;
230
-			}
231
-			$relativePath = $mount->getInternalPath($dir);
232
-			/** @var \OC\Files\Cache\Scanner $scanner */
233
-			$scanner = $storage->getScanner();
234
-			$scanner->setUseTransactions(false);
235
-			$this->attachListener($mount);
227
+            // don't scan received local shares, these can be scanned when scanning the owner's storage
228
+            if ($storage->instanceOfStorage(SharedStorage::class)) {
229
+                continue;
230
+            }
231
+            $relativePath = $mount->getInternalPath($dir);
232
+            /** @var \OC\Files\Cache\Scanner $scanner */
233
+            $scanner = $storage->getScanner();
234
+            $scanner->setUseTransactions(false);
235
+            $this->attachListener($mount);
236 236
 
237
-			$scanner->listen('\OC\Files\Cache\Scanner', 'removeFromCache', function ($path) use ($storage) {
238
-				$this->postProcessEntry($storage, $path);
239
-				$this->dispatcher->dispatchTyped(new NodeRemovedFromCache($storage, $path));
240
-			});
241
-			$scanner->listen('\OC\Files\Cache\Scanner', 'updateCache', function ($path) use ($storage) {
242
-				$this->postProcessEntry($storage, $path);
243
-				$this->dispatcher->dispatchTyped(new FileCacheUpdated($storage, $path));
244
-			});
245
-			$scanner->listen('\OC\Files\Cache\Scanner', 'addToCache', function ($path, $storageId, $data, $fileId) use ($storage) {
246
-				$this->postProcessEntry($storage, $path);
247
-				if ($fileId) {
248
-					$this->dispatcher->dispatchTyped(new FileCacheUpdated($storage, $path));
249
-				} else {
250
-					$this->dispatcher->dispatchTyped(new NodeAddedToCache($storage, $path));
251
-				}
252
-			});
237
+            $scanner->listen('\OC\Files\Cache\Scanner', 'removeFromCache', function ($path) use ($storage) {
238
+                $this->postProcessEntry($storage, $path);
239
+                $this->dispatcher->dispatchTyped(new NodeRemovedFromCache($storage, $path));
240
+            });
241
+            $scanner->listen('\OC\Files\Cache\Scanner', 'updateCache', function ($path) use ($storage) {
242
+                $this->postProcessEntry($storage, $path);
243
+                $this->dispatcher->dispatchTyped(new FileCacheUpdated($storage, $path));
244
+            });
245
+            $scanner->listen('\OC\Files\Cache\Scanner', 'addToCache', function ($path, $storageId, $data, $fileId) use ($storage) {
246
+                $this->postProcessEntry($storage, $path);
247
+                if ($fileId) {
248
+                    $this->dispatcher->dispatchTyped(new FileCacheUpdated($storage, $path));
249
+                } else {
250
+                    $this->dispatcher->dispatchTyped(new NodeAddedToCache($storage, $path));
251
+                }
252
+            });
253 253
 
254
-			if (!$storage->file_exists($relativePath)) {
255
-				throw new NotFoundException($dir);
256
-			}
254
+            if (!$storage->file_exists($relativePath)) {
255
+                throw new NotFoundException($dir);
256
+            }
257 257
 
258
-			if ($this->useTransaction) {
259
-				$this->db->beginTransaction();
260
-			}
261
-			try {
262
-				$propagator = $storage->getPropagator();
263
-				$propagator->beginBatch();
264
-				try {
265
-					$scanner->scan($relativePath, $recursive, \OC\Files\Cache\Scanner::REUSE_ETAG | \OC\Files\Cache\Scanner::REUSE_SIZE);
266
-				} catch (LockedException $e) {
267
-					if (is_string($e->getReadablePath()) && str_starts_with($e->getReadablePath(), 'scanner::')) {
268
-						throw new LockedException("scanner::$dir", $e, $e->getExistingLock());
269
-					} else {
270
-						throw $e;
271
-					}
272
-				}
273
-				$cache = $storage->getCache();
274
-				if ($cache instanceof Cache) {
275
-					// only re-calculate for the root folder we scanned, anything below that is taken care of by the scanner
276
-					$cache->correctFolderSize($relativePath);
277
-				}
278
-				$propagator->commitBatch();
279
-			} catch (StorageNotAvailableException $e) {
280
-				$this->logger->error('Storage ' . $storage->getId() . ' not available', ['exception' => $e]);
281
-				$this->emit('\OC\Files\Utils\Scanner', 'StorageNotAvailable', [$e]);
282
-			}
283
-			if ($this->useTransaction) {
284
-				$this->db->commit();
285
-			}
286
-		}
287
-	}
258
+            if ($this->useTransaction) {
259
+                $this->db->beginTransaction();
260
+            }
261
+            try {
262
+                $propagator = $storage->getPropagator();
263
+                $propagator->beginBatch();
264
+                try {
265
+                    $scanner->scan($relativePath, $recursive, \OC\Files\Cache\Scanner::REUSE_ETAG | \OC\Files\Cache\Scanner::REUSE_SIZE);
266
+                } catch (LockedException $e) {
267
+                    if (is_string($e->getReadablePath()) && str_starts_with($e->getReadablePath(), 'scanner::')) {
268
+                        throw new LockedException("scanner::$dir", $e, $e->getExistingLock());
269
+                    } else {
270
+                        throw $e;
271
+                    }
272
+                }
273
+                $cache = $storage->getCache();
274
+                if ($cache instanceof Cache) {
275
+                    // only re-calculate for the root folder we scanned, anything below that is taken care of by the scanner
276
+                    $cache->correctFolderSize($relativePath);
277
+                }
278
+                $propagator->commitBatch();
279
+            } catch (StorageNotAvailableException $e) {
280
+                $this->logger->error('Storage ' . $storage->getId() . ' not available', ['exception' => $e]);
281
+                $this->emit('\OC\Files\Utils\Scanner', 'StorageNotAvailable', [$e]);
282
+            }
283
+            if ($this->useTransaction) {
284
+                $this->db->commit();
285
+            }
286
+        }
287
+    }
288 288
 
289
-	private function triggerPropagator(IStorage $storage, $internalPath) {
290
-		$storage->getPropagator()->propagateChange($internalPath, time());
291
-	}
289
+    private function triggerPropagator(IStorage $storage, $internalPath) {
290
+        $storage->getPropagator()->propagateChange($internalPath, time());
291
+    }
292 292
 
293
-	private function postProcessEntry(IStorage $storage, $internalPath) {
294
-		$this->triggerPropagator($storage, $internalPath);
295
-		if ($this->useTransaction) {
296
-			$this->entriesToCommit++;
297
-			if ($this->entriesToCommit >= self::MAX_ENTRIES_TO_COMMIT) {
298
-				$propagator = $storage->getPropagator();
299
-				$this->entriesToCommit = 0;
300
-				$this->db->commit();
301
-				$propagator->commitBatch();
302
-				$this->db->beginTransaction();
303
-				$propagator->beginBatch();
304
-			}
305
-		}
306
-	}
293
+    private function postProcessEntry(IStorage $storage, $internalPath) {
294
+        $this->triggerPropagator($storage, $internalPath);
295
+        if ($this->useTransaction) {
296
+            $this->entriesToCommit++;
297
+            if ($this->entriesToCommit >= self::MAX_ENTRIES_TO_COMMIT) {
298
+                $propagator = $storage->getPropagator();
299
+                $this->entriesToCommit = 0;
300
+                $this->db->commit();
301
+                $propagator->commitBatch();
302
+                $this->db->beginTransaction();
303
+                $propagator->beginBatch();
304
+            }
305
+        }
306
+    }
307 307
 }
Please login to merge, or discard this patch.
apps/files/lib/Command/Scan.php 1 patch
Indentation   +274 added lines, -274 removed lines patch added patch discarded remove patch
@@ -34,304 +34,304 @@
 block discarded – undo
34 34
 use Symfony\Component\Console\Output\OutputInterface;
35 35
 
36 36
 class Scan extends Base {
37
-	protected float $execTime = 0;
38
-	protected int $foldersCounter = 0;
39
-	protected int $filesCounter = 0;
40
-	protected int $errorsCounter = 0;
41
-	protected int $newCounter = 0;
42
-	protected int $updatedCounter = 0;
43
-	protected int $removedCounter = 0;
37
+    protected float $execTime = 0;
38
+    protected int $foldersCounter = 0;
39
+    protected int $filesCounter = 0;
40
+    protected int $errorsCounter = 0;
41
+    protected int $newCounter = 0;
42
+    protected int $updatedCounter = 0;
43
+    protected int $removedCounter = 0;
44 44
 
45
-	public function __construct(
46
-		private IUserManager $userManager,
47
-		private IRootFolder $rootFolder,
48
-		private FilesMetadataManager $filesMetadataManager,
49
-		private IEventDispatcher $eventDispatcher,
50
-		private LoggerInterface $logger,
51
-	) {
52
-		parent::__construct();
53
-	}
45
+    public function __construct(
46
+        private IUserManager $userManager,
47
+        private IRootFolder $rootFolder,
48
+        private FilesMetadataManager $filesMetadataManager,
49
+        private IEventDispatcher $eventDispatcher,
50
+        private LoggerInterface $logger,
51
+    ) {
52
+        parent::__construct();
53
+    }
54 54
 
55
-	protected function configure(): void {
56
-		parent::configure();
55
+    protected function configure(): void {
56
+        parent::configure();
57 57
 
58
-		$this
59
-			->setName('files:scan')
60
-			->setDescription('rescan filesystem')
61
-			->addArgument(
62
-				'user_id',
63
-				InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
64
-				'will rescan all files of the given user(s)'
65
-			)
66
-			->addOption(
67
-				'path',
68
-				'p',
69
-				InputOption::VALUE_REQUIRED,
70
-				'limit rescan to this path, eg. --path="/alice/files/Music", the user_id is determined by the path and the user_id parameter and --all are ignored'
71
-			)
72
-			->addOption(
73
-				'generate-metadata',
74
-				null,
75
-				InputOption::VALUE_OPTIONAL,
76
-				'Generate metadata for all scanned files; if specified only generate for named value',
77
-				''
78
-			)
79
-			->addOption(
80
-				'all',
81
-				null,
82
-				InputOption::VALUE_NONE,
83
-				'will rescan all files of all known users'
84
-			)->addOption(
85
-				'unscanned',
86
-				null,
87
-				InputOption::VALUE_NONE,
88
-				'only scan files which are marked as not fully scanned'
89
-			)->addOption(
90
-				'shallow',
91
-				null,
92
-				InputOption::VALUE_NONE,
93
-				'do not scan folders recursively'
94
-			)->addOption(
95
-				'home-only',
96
-				null,
97
-				InputOption::VALUE_NONE,
98
-				'only scan the home storage, ignoring any mounted external storage or share'
99
-			);
100
-	}
58
+        $this
59
+            ->setName('files:scan')
60
+            ->setDescription('rescan filesystem')
61
+            ->addArgument(
62
+                'user_id',
63
+                InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
64
+                'will rescan all files of the given user(s)'
65
+            )
66
+            ->addOption(
67
+                'path',
68
+                'p',
69
+                InputOption::VALUE_REQUIRED,
70
+                'limit rescan to this path, eg. --path="/alice/files/Music", the user_id is determined by the path and the user_id parameter and --all are ignored'
71
+            )
72
+            ->addOption(
73
+                'generate-metadata',
74
+                null,
75
+                InputOption::VALUE_OPTIONAL,
76
+                'Generate metadata for all scanned files; if specified only generate for named value',
77
+                ''
78
+            )
79
+            ->addOption(
80
+                'all',
81
+                null,
82
+                InputOption::VALUE_NONE,
83
+                'will rescan all files of all known users'
84
+            )->addOption(
85
+                'unscanned',
86
+                null,
87
+                InputOption::VALUE_NONE,
88
+                'only scan files which are marked as not fully scanned'
89
+            )->addOption(
90
+                'shallow',
91
+                null,
92
+                InputOption::VALUE_NONE,
93
+                'do not scan folders recursively'
94
+            )->addOption(
95
+                'home-only',
96
+                null,
97
+                InputOption::VALUE_NONE,
98
+                'only scan the home storage, ignoring any mounted external storage or share'
99
+            );
100
+    }
101 101
 
102
-	protected function scanFiles(string $user, string $path, ?string $scanMetadata, OutputInterface $output, bool $backgroundScan = false, bool $recursive = true, bool $homeOnly = false): void {
103
-		$connection = $this->reconnectToDatabase($output);
104
-		$scanner = new Scanner(
105
-			$user,
106
-			new ConnectionAdapter($connection),
107
-			Server::get(IEventDispatcher::class),
108
-			Server::get(LoggerInterface::class)
109
-		);
102
+    protected function scanFiles(string $user, string $path, ?string $scanMetadata, OutputInterface $output, bool $backgroundScan = false, bool $recursive = true, bool $homeOnly = false): void {
103
+        $connection = $this->reconnectToDatabase($output);
104
+        $scanner = new Scanner(
105
+            $user,
106
+            new ConnectionAdapter($connection),
107
+            Server::get(IEventDispatcher::class),
108
+            Server::get(LoggerInterface::class)
109
+        );
110 110
 
111
-		# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
112
-		$scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function (string $path) use ($output, $scanMetadata): void {
113
-			$output->writeln("\tFile\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
114
-			++$this->filesCounter;
115
-			$this->abortIfInterrupted();
116
-			if ($scanMetadata !== null) {
117
-				$node = $this->rootFolder->get($path);
118
-				$this->filesMetadataManager->refreshMetadata(
119
-					$node,
120
-					($scanMetadata !== '') ? IFilesMetadataManager::PROCESS_NAMED : IFilesMetadataManager::PROCESS_LIVE | IFilesMetadataManager::PROCESS_BACKGROUND,
121
-					$scanMetadata
122
-				);
123
-			}
124
-		});
111
+        # check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
112
+        $scanner->listen('\OC\Files\Utils\Scanner', 'scanFile', function (string $path) use ($output, $scanMetadata): void {
113
+            $output->writeln("\tFile\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
114
+            ++$this->filesCounter;
115
+            $this->abortIfInterrupted();
116
+            if ($scanMetadata !== null) {
117
+                $node = $this->rootFolder->get($path);
118
+                $this->filesMetadataManager->refreshMetadata(
119
+                    $node,
120
+                    ($scanMetadata !== '') ? IFilesMetadataManager::PROCESS_NAMED : IFilesMetadataManager::PROCESS_LIVE | IFilesMetadataManager::PROCESS_BACKGROUND,
121
+                    $scanMetadata
122
+                );
123
+            }
124
+        });
125 125
 
126
-		$scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output): void {
127
-			$output->writeln("\tFolder\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
128
-			++$this->foldersCounter;
129
-			$this->abortIfInterrupted();
130
-		});
126
+        $scanner->listen('\OC\Files\Utils\Scanner', 'scanFolder', function ($path) use ($output): void {
127
+            $output->writeln("\tFolder\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
128
+            ++$this->foldersCounter;
129
+            $this->abortIfInterrupted();
130
+        });
131 131
 
132
-		$scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output): void {
133
-			$output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', OutputInterface::VERBOSITY_VERBOSE);
134
-			++$this->errorsCounter;
135
-		});
132
+        $scanner->listen('\OC\Files\Utils\Scanner', 'StorageNotAvailable', function (StorageNotAvailableException $e) use ($output): void {
133
+            $output->writeln('Error while scanning, storage not available (' . $e->getMessage() . ')', OutputInterface::VERBOSITY_VERBOSE);
134
+            ++$this->errorsCounter;
135
+        });
136 136
 
137
-		$scanner->listen('\OC\Files\Utils\Scanner', 'normalizedNameMismatch', function ($fullPath) use ($output): void {
138
-			$output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>');
139
-			++$this->errorsCounter;
140
-		});
137
+        $scanner->listen('\OC\Files\Utils\Scanner', 'normalizedNameMismatch', function ($fullPath) use ($output): void {
138
+            $output->writeln("\t<error>Entry \"" . $fullPath . '" will not be accessible due to incompatible encoding</error>');
139
+            ++$this->errorsCounter;
140
+        });
141 141
 
142
-		$this->eventDispatcher->addListener(NodeAddedToCache::class, function (): void {
143
-			++$this->newCounter;
144
-		});
145
-		$this->eventDispatcher->addListener(FileCacheUpdated::class, function (): void {
146
-			++$this->updatedCounter;
147
-		});
148
-		$this->eventDispatcher->addListener(NodeRemovedFromCache::class, function (): void {
149
-			++$this->removedCounter;
150
-		});
142
+        $this->eventDispatcher->addListener(NodeAddedToCache::class, function (): void {
143
+            ++$this->newCounter;
144
+        });
145
+        $this->eventDispatcher->addListener(FileCacheUpdated::class, function (): void {
146
+            ++$this->updatedCounter;
147
+        });
148
+        $this->eventDispatcher->addListener(NodeRemovedFromCache::class, function (): void {
149
+            ++$this->removedCounter;
150
+        });
151 151
 
152
-		try {
153
-			if ($backgroundScan) {
154
-				$scanner->backgroundScan($path);
155
-			} else {
156
-				$scanner->scan($path, $recursive, $homeOnly ? [$this, 'filterHomeMount'] : null);
157
-			}
158
-		} catch (ForbiddenException $e) {
159
-			$output->writeln("<error>Home storage for user $user not writable or 'files' subdirectory missing</error>");
160
-			$output->writeln('  ' . $e->getMessage());
161
-			$output->writeln('Make sure you\'re running the scan command only as the user the web server runs as');
162
-			++$this->errorsCounter;
163
-		} catch (InterruptedException $e) {
164
-			# exit the function if ctrl-c has been pressed
165
-			$output->writeln('Interrupted by user');
166
-		} catch (NotFoundException $e) {
167
-			$output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>');
168
-			++$this->errorsCounter;
169
-		} catch (LockedException $e) {
170
-			if (str_starts_with($e->getPath(), 'scanner::')) {
171
-				$output->writeln('<error>Another process is already scanning \'' . substr($e->getPath(), strlen('scanner::')) . '\'</error>');
172
-			} else {
173
-				throw $e;
174
-			}
175
-		} catch (\Exception $e) {
176
-			$output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>');
177
-			$output->writeln('<error>' . $e->getTraceAsString() . '</error>');
178
-			++$this->errorsCounter;
179
-		}
180
-	}
152
+        try {
153
+            if ($backgroundScan) {
154
+                $scanner->backgroundScan($path);
155
+            } else {
156
+                $scanner->scan($path, $recursive, $homeOnly ? [$this, 'filterHomeMount'] : null);
157
+            }
158
+        } catch (ForbiddenException $e) {
159
+            $output->writeln("<error>Home storage for user $user not writable or 'files' subdirectory missing</error>");
160
+            $output->writeln('  ' . $e->getMessage());
161
+            $output->writeln('Make sure you\'re running the scan command only as the user the web server runs as');
162
+            ++$this->errorsCounter;
163
+        } catch (InterruptedException $e) {
164
+            # exit the function if ctrl-c has been pressed
165
+            $output->writeln('Interrupted by user');
166
+        } catch (NotFoundException $e) {
167
+            $output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>');
168
+            ++$this->errorsCounter;
169
+        } catch (LockedException $e) {
170
+            if (str_starts_with($e->getPath(), 'scanner::')) {
171
+                $output->writeln('<error>Another process is already scanning \'' . substr($e->getPath(), strlen('scanner::')) . '\'</error>');
172
+            } else {
173
+                throw $e;
174
+            }
175
+        } catch (\Exception $e) {
176
+            $output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>');
177
+            $output->writeln('<error>' . $e->getTraceAsString() . '</error>');
178
+            ++$this->errorsCounter;
179
+        }
180
+    }
181 181
 
182
-	public function filterHomeMount(IMountPoint $mountPoint): bool {
183
-		// any mountpoint inside '/$user/files/'
184
-		return substr_count($mountPoint->getMountPoint(), '/') <= 3;
185
-	}
182
+    public function filterHomeMount(IMountPoint $mountPoint): bool {
183
+        // any mountpoint inside '/$user/files/'
184
+        return substr_count($mountPoint->getMountPoint(), '/') <= 3;
185
+    }
186 186
 
187
-	protected function execute(InputInterface $input, OutputInterface $output): int {
188
-		$inputPath = $input->getOption('path');
189
-		if ($inputPath) {
190
-			$inputPath = '/' . trim($inputPath, '/');
191
-			[, $user,] = explode('/', $inputPath, 3);
192
-			$users = [$user];
193
-		} elseif ($input->getOption('all')) {
194
-			$users = $this->userManager->search('');
195
-		} else {
196
-			$users = $input->getArgument('user_id');
197
-		}
187
+    protected function execute(InputInterface $input, OutputInterface $output): int {
188
+        $inputPath = $input->getOption('path');
189
+        if ($inputPath) {
190
+            $inputPath = '/' . trim($inputPath, '/');
191
+            [, $user,] = explode('/', $inputPath, 3);
192
+            $users = [$user];
193
+        } elseif ($input->getOption('all')) {
194
+            $users = $this->userManager->search('');
195
+        } else {
196
+            $users = $input->getArgument('user_id');
197
+        }
198 198
 
199
-		# check quantity of users to be process and show it on the command line
200
-		$users_total = count($users);
201
-		if ($users_total === 0) {
202
-			$output->writeln('<error>Please specify the user id to scan, --all to scan for all users or --path=...</error>');
203
-			return self::FAILURE;
204
-		}
199
+        # check quantity of users to be process and show it on the command line
200
+        $users_total = count($users);
201
+        if ($users_total === 0) {
202
+            $output->writeln('<error>Please specify the user id to scan, --all to scan for all users or --path=...</error>');
203
+            return self::FAILURE;
204
+        }
205 205
 
206
-		$this->initTools($output);
206
+        $this->initTools($output);
207 207
 
208
-		// getOption() logic on VALUE_OPTIONAL
209
-		$metadata = null; // null if --generate-metadata is not set, empty if option have no value, value if set
210
-		if ($input->getOption('generate-metadata') !== '') {
211
-			$metadata = $input->getOption('generate-metadata') ?? '';
212
-		}
208
+        // getOption() logic on VALUE_OPTIONAL
209
+        $metadata = null; // null if --generate-metadata is not set, empty if option have no value, value if set
210
+        if ($input->getOption('generate-metadata') !== '') {
211
+            $metadata = $input->getOption('generate-metadata') ?? '';
212
+        }
213 213
 
214
-		$user_count = 0;
215
-		foreach ($users as $user) {
216
-			if (is_object($user)) {
217
-				$user = $user->getUID();
218
-			}
219
-			$path = $inputPath ?: '/' . $user;
220
-			++$user_count;
221
-			if ($this->userManager->userExists($user)) {
222
-				$output->writeln("Starting scan for user $user_count out of $users_total ($user)");
223
-				$this->scanFiles($user, $path, $metadata, $output, $input->getOption('unscanned'), !$input->getOption('shallow'), $input->getOption('home-only'));
224
-				$output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
225
-			} else {
226
-				$output->writeln("<error>Unknown user $user_count $user</error>");
227
-				$output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
228
-			}
214
+        $user_count = 0;
215
+        foreach ($users as $user) {
216
+            if (is_object($user)) {
217
+                $user = $user->getUID();
218
+            }
219
+            $path = $inputPath ?: '/' . $user;
220
+            ++$user_count;
221
+            if ($this->userManager->userExists($user)) {
222
+                $output->writeln("Starting scan for user $user_count out of $users_total ($user)");
223
+                $this->scanFiles($user, $path, $metadata, $output, $input->getOption('unscanned'), !$input->getOption('shallow'), $input->getOption('home-only'));
224
+                $output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
225
+            } else {
226
+                $output->writeln("<error>Unknown user $user_count $user</error>");
227
+                $output->writeln('', OutputInterface::VERBOSITY_VERBOSE);
228
+            }
229 229
 
230
-			try {
231
-				$this->abortIfInterrupted();
232
-			} catch (InterruptedException $e) {
233
-				break;
234
-			}
235
-		}
230
+            try {
231
+                $this->abortIfInterrupted();
232
+            } catch (InterruptedException $e) {
233
+                break;
234
+            }
235
+        }
236 236
 
237
-		$this->presentStats($output);
238
-		return self::SUCCESS;
239
-	}
237
+        $this->presentStats($output);
238
+        return self::SUCCESS;
239
+    }
240 240
 
241
-	/**
242
-	 * Initialises some useful tools for the Command
243
-	 */
244
-	protected function initTools(OutputInterface $output): void {
245
-		// Start the timer
246
-		$this->execTime = -microtime(true);
247
-		// Convert PHP errors to exceptions
248
-		set_error_handler(
249
-			fn (int $severity, string $message, string $file, int $line): bool =>
250
-				$this->exceptionErrorHandler($output, $severity, $message, $file, $line),
251
-			E_ALL
252
-		);
253
-	}
241
+    /**
242
+     * Initialises some useful tools for the Command
243
+     */
244
+    protected function initTools(OutputInterface $output): void {
245
+        // Start the timer
246
+        $this->execTime = -microtime(true);
247
+        // Convert PHP errors to exceptions
248
+        set_error_handler(
249
+            fn (int $severity, string $message, string $file, int $line): bool =>
250
+                $this->exceptionErrorHandler($output, $severity, $message, $file, $line),
251
+            E_ALL
252
+        );
253
+    }
254 254
 
255
-	/**
256
-	 * Processes PHP errors in order to be able to show them in the output
257
-	 *
258
-	 * @see https://www.php.net/manual/en/function.set-error-handler.php
259
-	 *
260
-	 * @param int $severity the level of the error raised
261
-	 * @param string $message
262
-	 * @param string $file the filename that the error was raised in
263
-	 * @param int $line the line number the error was raised
264
-	 */
265
-	public function exceptionErrorHandler(OutputInterface $output, int $severity, string $message, string $file, int $line): bool {
266
-		if (($severity === E_DEPRECATED) || ($severity === E_USER_DEPRECATED)) {
267
-			// Do not show deprecation warnings
268
-			return false;
269
-		}
270
-		$e = new \ErrorException($message, 0, $severity, $file, $line);
271
-		$output->writeln('<error>Error during scan: ' . $e->getMessage() . '</error>');
272
-		$output->writeln('<error>' . $e->getTraceAsString() . '</error>', OutputInterface::VERBOSITY_VERY_VERBOSE);
273
-		++$this->errorsCounter;
274
-		return true;
275
-	}
255
+    /**
256
+     * Processes PHP errors in order to be able to show them in the output
257
+     *
258
+     * @see https://www.php.net/manual/en/function.set-error-handler.php
259
+     *
260
+     * @param int $severity the level of the error raised
261
+     * @param string $message
262
+     * @param string $file the filename that the error was raised in
263
+     * @param int $line the line number the error was raised
264
+     */
265
+    public function exceptionErrorHandler(OutputInterface $output, int $severity, string $message, string $file, int $line): bool {
266
+        if (($severity === E_DEPRECATED) || ($severity === E_USER_DEPRECATED)) {
267
+            // Do not show deprecation warnings
268
+            return false;
269
+        }
270
+        $e = new \ErrorException($message, 0, $severity, $file, $line);
271
+        $output->writeln('<error>Error during scan: ' . $e->getMessage() . '</error>');
272
+        $output->writeln('<error>' . $e->getTraceAsString() . '</error>', OutputInterface::VERBOSITY_VERY_VERBOSE);
273
+        ++$this->errorsCounter;
274
+        return true;
275
+    }
276 276
 
277
-	protected function presentStats(OutputInterface $output): void {
278
-		// Stop the timer
279
-		$this->execTime += microtime(true);
277
+    protected function presentStats(OutputInterface $output): void {
278
+        // Stop the timer
279
+        $this->execTime += microtime(true);
280 280
 
281
-		$this->logger->info("Completed scan of {$this->filesCounter} files in {$this->foldersCounter} folder. Found {$this->newCounter} new, {$this->updatedCounter} updated and {$this->removedCounter} removed items");
281
+        $this->logger->info("Completed scan of {$this->filesCounter} files in {$this->foldersCounter} folder. Found {$this->newCounter} new, {$this->updatedCounter} updated and {$this->removedCounter} removed items");
282 282
 
283
-		$headers = [
284
-			'Folders',
285
-			'Files',
286
-			'New',
287
-			'Updated',
288
-			'Removed',
289
-			'Errors',
290
-			'Elapsed time',
291
-		];
292
-		$niceDate = $this->formatExecTime();
293
-		$rows = [
294
-			$this->foldersCounter,
295
-			$this->filesCounter,
296
-			$this->newCounter,
297
-			$this->updatedCounter,
298
-			$this->removedCounter,
299
-			$this->errorsCounter,
300
-			$niceDate,
301
-		];
302
-		$table = new Table($output);
303
-		$table
304
-			->setHeaders($headers)
305
-			->setRows([$rows]);
306
-		$table->render();
307
-	}
283
+        $headers = [
284
+            'Folders',
285
+            'Files',
286
+            'New',
287
+            'Updated',
288
+            'Removed',
289
+            'Errors',
290
+            'Elapsed time',
291
+        ];
292
+        $niceDate = $this->formatExecTime();
293
+        $rows = [
294
+            $this->foldersCounter,
295
+            $this->filesCounter,
296
+            $this->newCounter,
297
+            $this->updatedCounter,
298
+            $this->removedCounter,
299
+            $this->errorsCounter,
300
+            $niceDate,
301
+        ];
302
+        $table = new Table($output);
303
+        $table
304
+            ->setHeaders($headers)
305
+            ->setRows([$rows]);
306
+        $table->render();
307
+    }
308 308
 
309 309
 
310
-	/**
311
-	 * Formats microtime into a human-readable format
312
-	 */
313
-	protected function formatExecTime(): string {
314
-		$secs = (int)round($this->execTime);
315
-		# convert seconds into HH:MM:SS form
316
-		return sprintf('%02d:%02d:%02d', (int)($secs / 3600), ((int)($secs / 60) % 60), $secs % 60);
317
-	}
310
+    /**
311
+     * Formats microtime into a human-readable format
312
+     */
313
+    protected function formatExecTime(): string {
314
+        $secs = (int)round($this->execTime);
315
+        # convert seconds into HH:MM:SS form
316
+        return sprintf('%02d:%02d:%02d', (int)($secs / 3600), ((int)($secs / 60) % 60), $secs % 60);
317
+    }
318 318
 
319
-	protected function reconnectToDatabase(OutputInterface $output): Connection {
320
-		/** @var Connection $connection */
321
-		$connection = Server::get(Connection::class);
322
-		try {
323
-			$connection->close();
324
-		} catch (\Exception $ex) {
325
-			$output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>");
326
-		}
327
-		while (!$connection->isConnected()) {
328
-			try {
329
-				$connection->connect();
330
-			} catch (\Exception $ex) {
331
-				$output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>");
332
-				sleep(60);
333
-			}
334
-		}
335
-		return $connection;
336
-	}
319
+    protected function reconnectToDatabase(OutputInterface $output): Connection {
320
+        /** @var Connection $connection */
321
+        $connection = Server::get(Connection::class);
322
+        try {
323
+            $connection->close();
324
+        } catch (\Exception $ex) {
325
+            $output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>");
326
+        }
327
+        while (!$connection->isConnected()) {
328
+            try {
329
+                $connection->connect();
330
+            } catch (\Exception $ex) {
331
+                $output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>");
332
+                sleep(60);
333
+            }
334
+        }
335
+        return $connection;
336
+    }
337 337
 }
Please login to merge, or discard this patch.
apps/files_external/lib/Command/Scan.php 1 patch
Indentation   +131 added lines, -131 removed lines patch added patch discarded remove patch
@@ -19,135 +19,135 @@
 block discarded – undo
19 19
 use Symfony\Component\Console\Output\OutputInterface;
20 20
 
21 21
 class Scan extends StorageAuthBase {
22
-	protected float $execTime = 0;
23
-	protected int $foldersCounter = 0;
24
-	protected int $filesCounter = 0;
25
-
26
-	public function __construct(
27
-		GlobalStoragesService $globalService,
28
-		IUserManager $userManager,
29
-	) {
30
-		parent::__construct($globalService, $userManager);
31
-	}
32
-
33
-	protected function configure(): void {
34
-		$this
35
-			->setName('files_external:scan')
36
-			->setDescription('Scan an external storage for changed files')
37
-			->addArgument(
38
-				'mount_id',
39
-				InputArgument::REQUIRED,
40
-				'the mount id of the mount to scan'
41
-			)->addOption(
42
-				'user',
43
-				'u',
44
-				InputOption::VALUE_REQUIRED,
45
-				'The username for the remote mount (required only for some mount configuration that don\'t store credentials)'
46
-			)->addOption(
47
-				'password',
48
-				'p',
49
-				InputOption::VALUE_REQUIRED,
50
-				'The password for the remote mount (required only for some mount configuration that don\'t store credentials)'
51
-			)->addOption(
52
-				'path',
53
-				'',
54
-				InputOption::VALUE_OPTIONAL,
55
-				'The path in the storage to scan',
56
-				''
57
-			);
58
-		parent::configure();
59
-	}
60
-
61
-	protected function execute(InputInterface $input, OutputInterface $output): int {
62
-		[, $storage] = $this->createStorage($input, $output);
63
-		if ($storage === null) {
64
-			return 1;
65
-		}
66
-
67
-		$path = $input->getOption('path');
68
-
69
-		$this->execTime = -microtime(true);
70
-
71
-		/** @var Scanner $scanner */
72
-		$scanner = $storage->getScanner();
73
-
74
-		$scanner->listen('\OC\Files\Cache\Scanner', 'scanFile', function (string $path) use ($output): void {
75
-			$output->writeln("\tFile\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
76
-			++$this->filesCounter;
77
-			$this->abortIfInterrupted();
78
-		});
79
-
80
-		$scanner->listen('\OC\Files\Cache\Scanner', 'scanFolder', function (string $path) use ($output): void {
81
-			$output->writeln("\tFolder\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
82
-			++$this->foldersCounter;
83
-			$this->abortIfInterrupted();
84
-		});
85
-
86
-		try {
87
-			$scanner->scan($path);
88
-		} catch (LockedException $e) {
89
-			if (is_string($e->getReadablePath()) && str_starts_with($e->getReadablePath(), 'scanner::')) {
90
-				if ($e->getReadablePath() === 'scanner::') {
91
-					$output->writeln('<error>Another process is already scanning this storage</error>');
92
-				} else {
93
-					$output->writeln('<error>Another process is already scanning \'' . substr($e->getReadablePath(), strlen('scanner::')) . '\' in this storage</error>');
94
-				}
95
-			} else {
96
-				throw $e;
97
-			}
98
-		}
99
-
100
-		$this->presentStats($output);
101
-
102
-		return 0;
103
-	}
104
-
105
-	/**
106
-	 * @param OutputInterface $output
107
-	 */
108
-	protected function presentStats(OutputInterface $output): void {
109
-		// Stop the timer
110
-		$this->execTime += microtime(true);
111
-
112
-		$headers = [
113
-			'Folders', 'Files', 'Elapsed time'
114
-		];
115
-
116
-		$this->showSummary($headers, [], $output);
117
-	}
118
-
119
-	/**
120
-	 * Shows a summary of operations
121
-	 *
122
-	 * @param string[] $headers
123
-	 * @param string[] $rows
124
-	 * @param OutputInterface $output
125
-	 */
126
-	protected function showSummary(array $headers, array $rows, OutputInterface $output): void {
127
-		$niceDate = $this->formatExecTime();
128
-		if (!$rows) {
129
-			$rows = [
130
-				$this->foldersCounter,
131
-				$this->filesCounter,
132
-				$niceDate,
133
-			];
134
-		}
135
-		$table = new Table($output);
136
-		$table
137
-			->setHeaders($headers)
138
-			->setRows([$rows]);
139
-		$table->render();
140
-	}
141
-
142
-
143
-	/**
144
-	 * Formats microtime into a human readable format
145
-	 *
146
-	 * @return string
147
-	 */
148
-	protected function formatExecTime(): string {
149
-		$secs = round($this->execTime);
150
-		# convert seconds into HH:MM:SS form
151
-		return sprintf('%02d:%02d:%02d', ($secs / 3600), ($secs / 60 % 60), $secs % 60);
152
-	}
22
+    protected float $execTime = 0;
23
+    protected int $foldersCounter = 0;
24
+    protected int $filesCounter = 0;
25
+
26
+    public function __construct(
27
+        GlobalStoragesService $globalService,
28
+        IUserManager $userManager,
29
+    ) {
30
+        parent::__construct($globalService, $userManager);
31
+    }
32
+
33
+    protected function configure(): void {
34
+        $this
35
+            ->setName('files_external:scan')
36
+            ->setDescription('Scan an external storage for changed files')
37
+            ->addArgument(
38
+                'mount_id',
39
+                InputArgument::REQUIRED,
40
+                'the mount id of the mount to scan'
41
+            )->addOption(
42
+                'user',
43
+                'u',
44
+                InputOption::VALUE_REQUIRED,
45
+                'The username for the remote mount (required only for some mount configuration that don\'t store credentials)'
46
+            )->addOption(
47
+                'password',
48
+                'p',
49
+                InputOption::VALUE_REQUIRED,
50
+                'The password for the remote mount (required only for some mount configuration that don\'t store credentials)'
51
+            )->addOption(
52
+                'path',
53
+                '',
54
+                InputOption::VALUE_OPTIONAL,
55
+                'The path in the storage to scan',
56
+                ''
57
+            );
58
+        parent::configure();
59
+    }
60
+
61
+    protected function execute(InputInterface $input, OutputInterface $output): int {
62
+        [, $storage] = $this->createStorage($input, $output);
63
+        if ($storage === null) {
64
+            return 1;
65
+        }
66
+
67
+        $path = $input->getOption('path');
68
+
69
+        $this->execTime = -microtime(true);
70
+
71
+        /** @var Scanner $scanner */
72
+        $scanner = $storage->getScanner();
73
+
74
+        $scanner->listen('\OC\Files\Cache\Scanner', 'scanFile', function (string $path) use ($output): void {
75
+            $output->writeln("\tFile\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
76
+            ++$this->filesCounter;
77
+            $this->abortIfInterrupted();
78
+        });
79
+
80
+        $scanner->listen('\OC\Files\Cache\Scanner', 'scanFolder', function (string $path) use ($output): void {
81
+            $output->writeln("\tFolder\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
82
+            ++$this->foldersCounter;
83
+            $this->abortIfInterrupted();
84
+        });
85
+
86
+        try {
87
+            $scanner->scan($path);
88
+        } catch (LockedException $e) {
89
+            if (is_string($e->getReadablePath()) && str_starts_with($e->getReadablePath(), 'scanner::')) {
90
+                if ($e->getReadablePath() === 'scanner::') {
91
+                    $output->writeln('<error>Another process is already scanning this storage</error>');
92
+                } else {
93
+                    $output->writeln('<error>Another process is already scanning \'' . substr($e->getReadablePath(), strlen('scanner::')) . '\' in this storage</error>');
94
+                }
95
+            } else {
96
+                throw $e;
97
+            }
98
+        }
99
+
100
+        $this->presentStats($output);
101
+
102
+        return 0;
103
+    }
104
+
105
+    /**
106
+     * @param OutputInterface $output
107
+     */
108
+    protected function presentStats(OutputInterface $output): void {
109
+        // Stop the timer
110
+        $this->execTime += microtime(true);
111
+
112
+        $headers = [
113
+            'Folders', 'Files', 'Elapsed time'
114
+        ];
115
+
116
+        $this->showSummary($headers, [], $output);
117
+    }
118
+
119
+    /**
120
+     * Shows a summary of operations
121
+     *
122
+     * @param string[] $headers
123
+     * @param string[] $rows
124
+     * @param OutputInterface $output
125
+     */
126
+    protected function showSummary(array $headers, array $rows, OutputInterface $output): void {
127
+        $niceDate = $this->formatExecTime();
128
+        if (!$rows) {
129
+            $rows = [
130
+                $this->foldersCounter,
131
+                $this->filesCounter,
132
+                $niceDate,
133
+            ];
134
+        }
135
+        $table = new Table($output);
136
+        $table
137
+            ->setHeaders($headers)
138
+            ->setRows([$rows]);
139
+        $table->render();
140
+    }
141
+
142
+
143
+    /**
144
+     * Formats microtime into a human readable format
145
+     *
146
+     * @return string
147
+     */
148
+    protected function formatExecTime(): string {
149
+        $secs = round($this->execTime);
150
+        # convert seconds into HH:MM:SS form
151
+        return sprintf('%02d:%02d:%02d', ($secs / 3600), ($secs / 60 % 60), $secs % 60);
152
+    }
153 153
 }
Please login to merge, or discard this patch.