Completed
Pull Request — master (#9395)
by Robin
16:14
created
lib/private/Files/Cache/Scanner.php 2 patches
Indentation   +500 added lines, -500 removed lines patch added patch discarded remove patch
@@ -53,504 +53,504 @@
 block discarded – undo
53 53
  * @package OC\Files\Cache
54 54
  */
55 55
 class Scanner extends BasicEmitter implements IScanner {
56
-	/**
57
-	 * @var \OC\Files\Storage\Storage $storage
58
-	 */
59
-	protected $storage;
60
-
61
-	/**
62
-	 * @var string $storageId
63
-	 */
64
-	protected $storageId;
65
-
66
-	/**
67
-	 * @var \OC\Files\Cache\Cache $cache
68
-	 */
69
-	protected $cache;
70
-
71
-	/**
72
-	 * @var boolean $cacheActive If true, perform cache operations, if false, do not affect cache
73
-	 */
74
-	protected $cacheActive;
75
-
76
-	/**
77
-	 * @var bool $useTransactions whether to use transactions
78
-	 */
79
-	protected $useTransactions = true;
80
-
81
-	/**
82
-	 * @var \OCP\Lock\ILockingProvider
83
-	 */
84
-	protected $lockingProvider;
85
-
86
-	public function __construct(\OC\Files\Storage\Storage $storage) {
87
-		$this->storage = $storage;
88
-		$this->storageId = $this->storage->getId();
89
-		$this->cache = $storage->getCache();
90
-		$this->cacheActive = !\OC::$server->getConfig()->getSystemValue('filesystem_cache_readonly', false);
91
-		$this->lockingProvider = \OC::$server->getLockingProvider();
92
-	}
93
-
94
-	/**
95
-	 * Whether to wrap the scanning of a folder in a database transaction
96
-	 * On default transactions are used
97
-	 *
98
-	 * @param bool $useTransactions
99
-	 */
100
-	public function setUseTransactions($useTransactions) {
101
-		$this->useTransactions = $useTransactions;
102
-	}
103
-
104
-	/**
105
-	 * get all the metadata of a file or folder
106
-	 * *
107
-	 *
108
-	 * @param string $path
109
-	 * @return array an array of metadata of the file
110
-	 */
111
-	protected function getData($path) {
112
-		$data = $this->storage->getMetaData($path);
113
-		if (is_null($data)) {
114
-			\OCP\Util::writeLog(Scanner::class, "!!! Path '$path' is not accessible or present !!!", ILogger::DEBUG);
115
-		}
116
-		return $data;
117
-	}
118
-
119
-	/**
120
-	 * scan a single file and store it in the cache
121
-	 *
122
-	 * @param string $file
123
-	 * @param int $reuseExisting
124
-	 * @param int $parentId
125
-	 * @param array | null $cacheData existing data in the cache for the file to be scanned
126
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
127
-	 * @return array an array of metadata of the scanned file
128
-	 * @throws \OC\ServerNotAvailableException
129
-	 * @throws \OCP\Lock\LockedException
130
-	 */
131
-	public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true) {
132
-		if ($file !== '') {
133
-			try {
134
-				$this->storage->verifyPath(dirname($file), basename($file));
135
-			} catch (\Exception $e) {
136
-				return null;
137
-			}
138
-		}
139
-		// only proceed if $file is not a partial file nor a blacklisted file
140
-		if (!self::isPartialFile($file) and !Filesystem::isFileBlacklisted($file)) {
141
-
142
-			//acquire a lock
143
-			if ($lock) {
144
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
145
-					$this->storage->acquireLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
146
-				}
147
-			}
148
-
149
-			try {
150
-				$data = $this->getData($file);
151
-			} catch (ForbiddenException $e) {
152
-				if ($lock) {
153
-					if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
154
-						$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
155
-					}
156
-				}
157
-
158
-				return null;
159
-			}
160
-
161
-			try {
162
-				if ($data) {
163
-
164
-					// pre-emit only if it was a file. By that we avoid counting/treating folders as files
165
-					if ($data['mimetype'] !== 'httpd/unix-directory') {
166
-						$this->emit('\OC\Files\Cache\Scanner', 'scanFile', array($file, $this->storageId));
167
-						\OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', array('path' => $file, 'storage' => $this->storageId));
168
-					}
169
-
170
-					$parent = dirname($file);
171
-					if ($parent === '.' or $parent === '/') {
172
-						$parent = '';
173
-					}
174
-					if ($parentId === -1) {
175
-						$parentId = $this->cache->getParentId($file);
176
-					}
177
-
178
-					// scan the parent if it's not in the cache (id -1) and the current file is not the root folder
179
-					if ($file and $parentId === -1) {
180
-						$parentData = $this->scanFile($parent);
181
-						if (!$parentData) {
182
-							return null;
183
-						}
184
-						$parentId = $parentData['fileid'];
185
-					}
186
-					if ($parent) {
187
-						$data['parent'] = $parentId;
188
-					}
189
-					if (is_null($cacheData)) {
190
-						/** @var CacheEntry $cacheData */
191
-						$cacheData = $this->cache->get($file);
192
-					}
193
-					if ($cacheData and $reuseExisting and isset($cacheData['fileid'])) {
194
-						// prevent empty etag
195
-						if (empty($cacheData['etag'])) {
196
-							$etag = $data['etag'];
197
-						} else {
198
-							$etag = $cacheData['etag'];
199
-						}
200
-						$fileId = $cacheData['fileid'];
201
-						$data['fileid'] = $fileId;
202
-						// only reuse data if the file hasn't explicitly changed
203
-						if (isset($data['storage_mtime']) && isset($cacheData['storage_mtime']) && $data['storage_mtime'] === $cacheData['storage_mtime']) {
204
-							$data['mtime'] = $cacheData['mtime'];
205
-							if (($reuseExisting & self::REUSE_SIZE) && ($data['size'] === -1)) {
206
-								$data['size'] = $cacheData['size'];
207
-							}
208
-							if ($reuseExisting & self::REUSE_ETAG) {
209
-								$data['etag'] = $etag;
210
-							}
211
-						}
212
-						// Only update metadata that has changed
213
-						$newData = array_diff_assoc($data, $cacheData->getData());
214
-					} else {
215
-						$newData = $data;
216
-						$fileId = -1;
217
-					}
218
-					if (!empty($newData)) {
219
-						// Reset the checksum if the data has changed
220
-						$newData['checksum'] = '';
221
-						$data['fileid'] = $this->addToCache($file, $newData, $fileId);
222
-					}
223
-					if (isset($cacheData['size'])) {
224
-						$data['oldSize'] = $cacheData['size'];
225
-					} else {
226
-						$data['oldSize'] = 0;
227
-					}
228
-
229
-					if (isset($cacheData['encrypted'])) {
230
-						$data['encrypted'] = $cacheData['encrypted'];
231
-					}
232
-
233
-					// post-emit only if it was a file. By that we avoid counting/treating folders as files
234
-					if ($data['mimetype'] !== 'httpd/unix-directory') {
235
-						$this->emit('\OC\Files\Cache\Scanner', 'postScanFile', array($file, $this->storageId));
236
-						\OC_Hook::emit('\OC\Files\Cache\Scanner', 'post_scan_file', array('path' => $file, 'storage' => $this->storageId));
237
-					}
238
-
239
-				} else {
240
-					$this->removeFromCache($file);
241
-				}
242
-			} catch (\Exception $e) {
243
-				if ($lock) {
244
-					if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
245
-						$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
246
-					}
247
-				}
248
-				throw $e;
249
-			}
250
-
251
-			//release the acquired lock
252
-			if ($lock) {
253
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
254
-					$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
255
-				}
256
-			}
257
-
258
-			if ($data && !isset($data['encrypted'])) {
259
-				$data['encrypted'] = false;
260
-			}
261
-			return $data;
262
-		}
263
-
264
-		return null;
265
-	}
266
-
267
-	protected function removeFromCache($path) {
268
-		\OC_Hook::emit('Scanner', 'removeFromCache', array('file' => $path));
269
-		$this->emit('\OC\Files\Cache\Scanner', 'removeFromCache', array($path));
270
-		if ($this->cacheActive) {
271
-			$this->cache->remove($path);
272
-		}
273
-	}
274
-
275
-	/**
276
-	 * @param string $path
277
-	 * @param array $data
278
-	 * @param int $fileId
279
-	 * @return int the id of the added file
280
-	 */
281
-	protected function addToCache($path, $data, $fileId = -1) {
282
-		if (isset($data['scan_permissions'])) {
283
-			$data['permissions'] = $data['scan_permissions'];
284
-		}
285
-		\OC_Hook::emit('Scanner', 'addToCache', array('file' => $path, 'data' => $data));
286
-		$this->emit('\OC\Files\Cache\Scanner', 'addToCache', array($path, $this->storageId, $data));
287
-		if ($this->cacheActive) {
288
-			if ($fileId !== -1) {
289
-				$this->cache->update($fileId, $data);
290
-				return $fileId;
291
-			} else {
292
-				return $this->cache->put($path, $data);
293
-			}
294
-		} else {
295
-			return -1;
296
-		}
297
-	}
298
-
299
-	/**
300
-	 * @param string $path
301
-	 * @param array $data
302
-	 * @param int $fileId
303
-	 */
304
-	protected function updateCache($path, $data, $fileId = -1) {
305
-		\OC_Hook::emit('Scanner', 'addToCache', array('file' => $path, 'data' => $data));
306
-		$this->emit('\OC\Files\Cache\Scanner', 'updateCache', array($path, $this->storageId, $data));
307
-		if ($this->cacheActive) {
308
-			if ($fileId !== -1) {
309
-				$this->cache->update($fileId, $data);
310
-			} else {
311
-				$this->cache->put($path, $data);
312
-			}
313
-		}
314
-	}
315
-
316
-	/**
317
-	 * scan a folder and all it's children
318
-	 *
319
-	 * @param string $path
320
-	 * @param bool $recursive
321
-	 * @param int $reuse
322
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
323
-	 * @return array an array of the meta data of the scanned file or folder
324
-	 */
325
-	public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
326
-		if ($reuse === -1) {
327
-			$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
328
-		}
329
-		if ($lock) {
330
-			if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
331
-				$this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
332
-				$this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
333
-			}
334
-		}
335
-		try {
336
-			$data = $this->scanFile($path, $reuse, -1, null, $lock);
337
-			if ($data and $data['mimetype'] === 'httpd/unix-directory') {
338
-				$size = $this->scanChildren($path, $recursive, $reuse, $data['fileid'], $lock);
339
-				$data['size'] = $size;
340
-			}
341
-		} finally {
342
-			if ($lock) {
343
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
344
-					$this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
345
-					$this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
346
-				}
347
-			}
348
-		}
349
-		return $data;
350
-	}
351
-
352
-	/**
353
-	 * Get the children currently in the cache
354
-	 *
355
-	 * @param int $folderId
356
-	 * @return array[]
357
-	 */
358
-	protected function getExistingChildren($folderId) {
359
-		$existingChildren = array();
360
-		$children = $this->cache->getFolderContentsById($folderId);
361
-		foreach ($children as $child) {
362
-			$existingChildren[$child['name']] = $child;
363
-		}
364
-		return $existingChildren;
365
-	}
366
-
367
-	/**
368
-	 * Get the children from the storage
369
-	 *
370
-	 * @param string $folder
371
-	 * @return string[]
372
-	 */
373
-	protected function getNewChildren($folder) {
374
-		$children = array();
375
-		if ($dh = $this->storage->opendir($folder)) {
376
-			if (is_resource($dh)) {
377
-				while (($file = readdir($dh)) !== false) {
378
-					if (!Filesystem::isIgnoredDir($file)) {
379
-						$children[] = trim(\OC\Files\Filesystem::normalizePath($file), '/');
380
-					}
381
-				}
382
-			}
383
-		}
384
-		return $children;
385
-	}
386
-
387
-	/**
388
-	 * scan all the files and folders in a folder
389
-	 *
390
-	 * @param string $path
391
-	 * @param bool $recursive
392
-	 * @param int $reuse
393
-	 * @param int $folderId id for the folder to be scanned
394
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
395
-	 * @return int the size of the scanned folder or -1 if the size is unknown at this stage
396
-	 */
397
-	protected function scanChildren($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $folderId = null, $lock = true) {
398
-		if ($reuse === -1) {
399
-			$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
400
-		}
401
-		$this->emit('\OC\Files\Cache\Scanner', 'scanFolder', array($path, $this->storageId));
402
-		$size = 0;
403
-		if (!is_null($folderId)) {
404
-			$folderId = $this->cache->getId($path);
405
-		}
406
-		$childQueue = $this->handleChildren($path, $recursive, $reuse, $folderId, $lock, $size);
407
-
408
-		foreach ($childQueue as $child => $childId) {
409
-			$childSize = $this->scanChildren($child, $recursive, $reuse, $childId, $lock);
410
-			if ($childSize === -1) {
411
-				$size = -1;
412
-			} else if ($size !== -1) {
413
-				$size += $childSize;
414
-			}
415
-		}
416
-		if ($this->cacheActive) {
417
-			$this->cache->update($folderId, array('size' => $size));
418
-		}
419
-		$this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', array($path, $this->storageId));
420
-		return $size;
421
-	}
422
-
423
-	private function handleChildren($path, $recursive, $reuse, $folderId, $lock, &$size) {
424
-		// we put this in it's own function so it cleans up the memory before we start recursing
425
-		$existingChildren = $this->getExistingChildren($folderId);
426
-		$newChildren = $this->getNewChildren($path);
427
-
428
-		if ($this->useTransactions) {
429
-			\OC::$server->getDatabaseConnection()->beginTransaction();
430
-		}
431
-
432
-		$exceptionOccurred = false;
433
-		$childQueue = [];
434
-		foreach ($newChildren as $file) {
435
-			$child = $path ? $path . '/' . $file : $file;
436
-			try {
437
-				$existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : null;
438
-				$data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock);
439
-				if ($data) {
440
-					if ($data['mimetype'] === 'httpd/unix-directory' and $recursive === self::SCAN_RECURSIVE) {
441
-						$childQueue[$child] = $data['fileid'];
442
-					} else if ($data['mimetype'] === 'httpd/unix-directory' and $recursive === self::SCAN_RECURSIVE_INCOMPLETE and $data['size'] === -1) {
443
-						// only recurse into folders which aren't fully scanned
444
-						$childQueue[$child] = $data['fileid'];
445
-					} else if ($data['size'] === -1) {
446
-						$size = -1;
447
-					} else if ($size !== -1) {
448
-						$size += $data['size'];
449
-					}
450
-				}
451
-			} catch (\Doctrine\DBAL\DBALException $ex) {
452
-				// might happen if inserting duplicate while a scanning
453
-				// process is running in parallel
454
-				// log and ignore
455
-				if ($this->useTransactions) {
456
-					\OC::$server->getDatabaseConnection()->rollback();
457
-					\OC::$server->getDatabaseConnection()->beginTransaction();
458
-				}
459
-				\OC::$server->getLogger()->logException($ex, [
460
-					'message' => 'Exception while scanning file "' . $child . '"',
461
-					'level' => ILogger::DEBUG,
462
-					'app' => 'core',
463
-				]);
464
-				$exceptionOccurred = true;
465
-			} catch (\OCP\Lock\LockedException $e) {
466
-				if ($this->useTransactions) {
467
-					\OC::$server->getDatabaseConnection()->rollback();
468
-				}
469
-				throw $e;
470
-			}
471
-		}
472
-		$removedChildren = \array_diff(array_keys($existingChildren), $newChildren);
473
-		foreach ($removedChildren as $childName) {
474
-			$child = $path ? $path . '/' . $childName : $childName;
475
-			$this->removeFromCache($child);
476
-		}
477
-		if ($this->useTransactions) {
478
-			\OC::$server->getDatabaseConnection()->commit();
479
-		}
480
-		if ($exceptionOccurred) {
481
-			// It might happen that the parallel scan process has already
482
-			// inserted mimetypes but those weren't available yet inside the transaction
483
-			// To make sure to have the updated mime types in such cases,
484
-			// we reload them here
485
-			\OC::$server->getMimeTypeLoader()->reset();
486
-		}
487
-		return $childQueue;
488
-	}
489
-
490
-	/**
491
-	 * check if the file should be ignored when scanning
492
-	 * NOTE: files with a '.part' extension are ignored as well!
493
-	 *       prevents unfinished put requests to be scanned
494
-	 *
495
-	 * @param string $file
496
-	 * @return boolean
497
-	 */
498
-	public static function isPartialFile($file) {
499
-		if (pathinfo($file, PATHINFO_EXTENSION) === 'part') {
500
-			return true;
501
-		}
502
-		if (strpos($file, '.part/') !== false) {
503
-			return true;
504
-		}
505
-
506
-		return false;
507
-	}
508
-
509
-	/**
510
-	 * walk over any folders that are not fully scanned yet and scan them
511
-	 */
512
-	public function backgroundScan() {
513
-		if (!$this->cache->inCache('')) {
514
-			$this->runBackgroundScanJob(function () {
515
-				$this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
516
-			}, '');
517
-		} else {
518
-			$lastPath = null;
519
-			while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
520
-				$this->runBackgroundScanJob(function () use ($path) {
521
-					$this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
522
-				}, $path);
523
-				// FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
524
-				// to make this possible
525
-				$lastPath = $path;
526
-			}
527
-		}
528
-	}
529
-
530
-	private function runBackgroundScanJob(callable $callback, $path) {
531
-		try {
532
-			$callback();
533
-			\OC_Hook::emit('Scanner', 'correctFolderSize', array('path' => $path));
534
-			if ($this->cacheActive && $this->cache instanceof Cache) {
535
-				$this->cache->correctFolderSize($path);
536
-			}
537
-		} catch (\OCP\Files\StorageInvalidException $e) {
538
-			// skip unavailable storages
539
-		} catch (\OCP\Files\StorageNotAvailableException $e) {
540
-			// skip unavailable storages
541
-		} catch (\OCP\Files\ForbiddenException $e) {
542
-			// skip forbidden storages
543
-		} catch (\OCP\Lock\LockedException $e) {
544
-			// skip unavailable storages
545
-		}
546
-	}
547
-
548
-	/**
549
-	 * Set whether the cache is affected by scan operations
550
-	 *
551
-	 * @param boolean $active The active state of the cache
552
-	 */
553
-	public function setCacheActive($active) {
554
-		$this->cacheActive = $active;
555
-	}
56
+    /**
57
+     * @var \OC\Files\Storage\Storage $storage
58
+     */
59
+    protected $storage;
60
+
61
+    /**
62
+     * @var string $storageId
63
+     */
64
+    protected $storageId;
65
+
66
+    /**
67
+     * @var \OC\Files\Cache\Cache $cache
68
+     */
69
+    protected $cache;
70
+
71
+    /**
72
+     * @var boolean $cacheActive If true, perform cache operations, if false, do not affect cache
73
+     */
74
+    protected $cacheActive;
75
+
76
+    /**
77
+     * @var bool $useTransactions whether to use transactions
78
+     */
79
+    protected $useTransactions = true;
80
+
81
+    /**
82
+     * @var \OCP\Lock\ILockingProvider
83
+     */
84
+    protected $lockingProvider;
85
+
86
+    public function __construct(\OC\Files\Storage\Storage $storage) {
87
+        $this->storage = $storage;
88
+        $this->storageId = $this->storage->getId();
89
+        $this->cache = $storage->getCache();
90
+        $this->cacheActive = !\OC::$server->getConfig()->getSystemValue('filesystem_cache_readonly', false);
91
+        $this->lockingProvider = \OC::$server->getLockingProvider();
92
+    }
93
+
94
+    /**
95
+     * Whether to wrap the scanning of a folder in a database transaction
96
+     * On default transactions are used
97
+     *
98
+     * @param bool $useTransactions
99
+     */
100
+    public function setUseTransactions($useTransactions) {
101
+        $this->useTransactions = $useTransactions;
102
+    }
103
+
104
+    /**
105
+     * get all the metadata of a file or folder
106
+     * *
107
+     *
108
+     * @param string $path
109
+     * @return array an array of metadata of the file
110
+     */
111
+    protected function getData($path) {
112
+        $data = $this->storage->getMetaData($path);
113
+        if (is_null($data)) {
114
+            \OCP\Util::writeLog(Scanner::class, "!!! Path '$path' is not accessible or present !!!", ILogger::DEBUG);
115
+        }
116
+        return $data;
117
+    }
118
+
119
+    /**
120
+     * scan a single file and store it in the cache
121
+     *
122
+     * @param string $file
123
+     * @param int $reuseExisting
124
+     * @param int $parentId
125
+     * @param array | null $cacheData existing data in the cache for the file to be scanned
126
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
127
+     * @return array an array of metadata of the scanned file
128
+     * @throws \OC\ServerNotAvailableException
129
+     * @throws \OCP\Lock\LockedException
130
+     */
131
+    public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true) {
132
+        if ($file !== '') {
133
+            try {
134
+                $this->storage->verifyPath(dirname($file), basename($file));
135
+            } catch (\Exception $e) {
136
+                return null;
137
+            }
138
+        }
139
+        // only proceed if $file is not a partial file nor a blacklisted file
140
+        if (!self::isPartialFile($file) and !Filesystem::isFileBlacklisted($file)) {
141
+
142
+            //acquire a lock
143
+            if ($lock) {
144
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
145
+                    $this->storage->acquireLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
146
+                }
147
+            }
148
+
149
+            try {
150
+                $data = $this->getData($file);
151
+            } catch (ForbiddenException $e) {
152
+                if ($lock) {
153
+                    if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
154
+                        $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
155
+                    }
156
+                }
157
+
158
+                return null;
159
+            }
160
+
161
+            try {
162
+                if ($data) {
163
+
164
+                    // pre-emit only if it was a file. By that we avoid counting/treating folders as files
165
+                    if ($data['mimetype'] !== 'httpd/unix-directory') {
166
+                        $this->emit('\OC\Files\Cache\Scanner', 'scanFile', array($file, $this->storageId));
167
+                        \OC_Hook::emit('\OC\Files\Cache\Scanner', 'scan_file', array('path' => $file, 'storage' => $this->storageId));
168
+                    }
169
+
170
+                    $parent = dirname($file);
171
+                    if ($parent === '.' or $parent === '/') {
172
+                        $parent = '';
173
+                    }
174
+                    if ($parentId === -1) {
175
+                        $parentId = $this->cache->getParentId($file);
176
+                    }
177
+
178
+                    // scan the parent if it's not in the cache (id -1) and the current file is not the root folder
179
+                    if ($file and $parentId === -1) {
180
+                        $parentData = $this->scanFile($parent);
181
+                        if (!$parentData) {
182
+                            return null;
183
+                        }
184
+                        $parentId = $parentData['fileid'];
185
+                    }
186
+                    if ($parent) {
187
+                        $data['parent'] = $parentId;
188
+                    }
189
+                    if (is_null($cacheData)) {
190
+                        /** @var CacheEntry $cacheData */
191
+                        $cacheData = $this->cache->get($file);
192
+                    }
193
+                    if ($cacheData and $reuseExisting and isset($cacheData['fileid'])) {
194
+                        // prevent empty etag
195
+                        if (empty($cacheData['etag'])) {
196
+                            $etag = $data['etag'];
197
+                        } else {
198
+                            $etag = $cacheData['etag'];
199
+                        }
200
+                        $fileId = $cacheData['fileid'];
201
+                        $data['fileid'] = $fileId;
202
+                        // only reuse data if the file hasn't explicitly changed
203
+                        if (isset($data['storage_mtime']) && isset($cacheData['storage_mtime']) && $data['storage_mtime'] === $cacheData['storage_mtime']) {
204
+                            $data['mtime'] = $cacheData['mtime'];
205
+                            if (($reuseExisting & self::REUSE_SIZE) && ($data['size'] === -1)) {
206
+                                $data['size'] = $cacheData['size'];
207
+                            }
208
+                            if ($reuseExisting & self::REUSE_ETAG) {
209
+                                $data['etag'] = $etag;
210
+                            }
211
+                        }
212
+                        // Only update metadata that has changed
213
+                        $newData = array_diff_assoc($data, $cacheData->getData());
214
+                    } else {
215
+                        $newData = $data;
216
+                        $fileId = -1;
217
+                    }
218
+                    if (!empty($newData)) {
219
+                        // Reset the checksum if the data has changed
220
+                        $newData['checksum'] = '';
221
+                        $data['fileid'] = $this->addToCache($file, $newData, $fileId);
222
+                    }
223
+                    if (isset($cacheData['size'])) {
224
+                        $data['oldSize'] = $cacheData['size'];
225
+                    } else {
226
+                        $data['oldSize'] = 0;
227
+                    }
228
+
229
+                    if (isset($cacheData['encrypted'])) {
230
+                        $data['encrypted'] = $cacheData['encrypted'];
231
+                    }
232
+
233
+                    // post-emit only if it was a file. By that we avoid counting/treating folders as files
234
+                    if ($data['mimetype'] !== 'httpd/unix-directory') {
235
+                        $this->emit('\OC\Files\Cache\Scanner', 'postScanFile', array($file, $this->storageId));
236
+                        \OC_Hook::emit('\OC\Files\Cache\Scanner', 'post_scan_file', array('path' => $file, 'storage' => $this->storageId));
237
+                    }
238
+
239
+                } else {
240
+                    $this->removeFromCache($file);
241
+                }
242
+            } catch (\Exception $e) {
243
+                if ($lock) {
244
+                    if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
245
+                        $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
246
+                    }
247
+                }
248
+                throw $e;
249
+            }
250
+
251
+            //release the acquired lock
252
+            if ($lock) {
253
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
254
+                    $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
255
+                }
256
+            }
257
+
258
+            if ($data && !isset($data['encrypted'])) {
259
+                $data['encrypted'] = false;
260
+            }
261
+            return $data;
262
+        }
263
+
264
+        return null;
265
+    }
266
+
267
+    protected function removeFromCache($path) {
268
+        \OC_Hook::emit('Scanner', 'removeFromCache', array('file' => $path));
269
+        $this->emit('\OC\Files\Cache\Scanner', 'removeFromCache', array($path));
270
+        if ($this->cacheActive) {
271
+            $this->cache->remove($path);
272
+        }
273
+    }
274
+
275
+    /**
276
+     * @param string $path
277
+     * @param array $data
278
+     * @param int $fileId
279
+     * @return int the id of the added file
280
+     */
281
+    protected function addToCache($path, $data, $fileId = -1) {
282
+        if (isset($data['scan_permissions'])) {
283
+            $data['permissions'] = $data['scan_permissions'];
284
+        }
285
+        \OC_Hook::emit('Scanner', 'addToCache', array('file' => $path, 'data' => $data));
286
+        $this->emit('\OC\Files\Cache\Scanner', 'addToCache', array($path, $this->storageId, $data));
287
+        if ($this->cacheActive) {
288
+            if ($fileId !== -1) {
289
+                $this->cache->update($fileId, $data);
290
+                return $fileId;
291
+            } else {
292
+                return $this->cache->put($path, $data);
293
+            }
294
+        } else {
295
+            return -1;
296
+        }
297
+    }
298
+
299
+    /**
300
+     * @param string $path
301
+     * @param array $data
302
+     * @param int $fileId
303
+     */
304
+    protected function updateCache($path, $data, $fileId = -1) {
305
+        \OC_Hook::emit('Scanner', 'addToCache', array('file' => $path, 'data' => $data));
306
+        $this->emit('\OC\Files\Cache\Scanner', 'updateCache', array($path, $this->storageId, $data));
307
+        if ($this->cacheActive) {
308
+            if ($fileId !== -1) {
309
+                $this->cache->update($fileId, $data);
310
+            } else {
311
+                $this->cache->put($path, $data);
312
+            }
313
+        }
314
+    }
315
+
316
+    /**
317
+     * scan a folder and all it's children
318
+     *
319
+     * @param string $path
320
+     * @param bool $recursive
321
+     * @param int $reuse
322
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
323
+     * @return array an array of the meta data of the scanned file or folder
324
+     */
325
+    public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
326
+        if ($reuse === -1) {
327
+            $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
328
+        }
329
+        if ($lock) {
330
+            if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
331
+                $this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
332
+                $this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
333
+            }
334
+        }
335
+        try {
336
+            $data = $this->scanFile($path, $reuse, -1, null, $lock);
337
+            if ($data and $data['mimetype'] === 'httpd/unix-directory') {
338
+                $size = $this->scanChildren($path, $recursive, $reuse, $data['fileid'], $lock);
339
+                $data['size'] = $size;
340
+            }
341
+        } finally {
342
+            if ($lock) {
343
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
344
+                    $this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
345
+                    $this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
346
+                }
347
+            }
348
+        }
349
+        return $data;
350
+    }
351
+
352
+    /**
353
+     * Get the children currently in the cache
354
+     *
355
+     * @param int $folderId
356
+     * @return array[]
357
+     */
358
+    protected function getExistingChildren($folderId) {
359
+        $existingChildren = array();
360
+        $children = $this->cache->getFolderContentsById($folderId);
361
+        foreach ($children as $child) {
362
+            $existingChildren[$child['name']] = $child;
363
+        }
364
+        return $existingChildren;
365
+    }
366
+
367
+    /**
368
+     * Get the children from the storage
369
+     *
370
+     * @param string $folder
371
+     * @return string[]
372
+     */
373
+    protected function getNewChildren($folder) {
374
+        $children = array();
375
+        if ($dh = $this->storage->opendir($folder)) {
376
+            if (is_resource($dh)) {
377
+                while (($file = readdir($dh)) !== false) {
378
+                    if (!Filesystem::isIgnoredDir($file)) {
379
+                        $children[] = trim(\OC\Files\Filesystem::normalizePath($file), '/');
380
+                    }
381
+                }
382
+            }
383
+        }
384
+        return $children;
385
+    }
386
+
387
+    /**
388
+     * scan all the files and folders in a folder
389
+     *
390
+     * @param string $path
391
+     * @param bool $recursive
392
+     * @param int $reuse
393
+     * @param int $folderId id for the folder to be scanned
394
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
395
+     * @return int the size of the scanned folder or -1 if the size is unknown at this stage
396
+     */
397
+    protected function scanChildren($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $folderId = null, $lock = true) {
398
+        if ($reuse === -1) {
399
+            $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
400
+        }
401
+        $this->emit('\OC\Files\Cache\Scanner', 'scanFolder', array($path, $this->storageId));
402
+        $size = 0;
403
+        if (!is_null($folderId)) {
404
+            $folderId = $this->cache->getId($path);
405
+        }
406
+        $childQueue = $this->handleChildren($path, $recursive, $reuse, $folderId, $lock, $size);
407
+
408
+        foreach ($childQueue as $child => $childId) {
409
+            $childSize = $this->scanChildren($child, $recursive, $reuse, $childId, $lock);
410
+            if ($childSize === -1) {
411
+                $size = -1;
412
+            } else if ($size !== -1) {
413
+                $size += $childSize;
414
+            }
415
+        }
416
+        if ($this->cacheActive) {
417
+            $this->cache->update($folderId, array('size' => $size));
418
+        }
419
+        $this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', array($path, $this->storageId));
420
+        return $size;
421
+    }
422
+
423
+    private function handleChildren($path, $recursive, $reuse, $folderId, $lock, &$size) {
424
+        // we put this in it's own function so it cleans up the memory before we start recursing
425
+        $existingChildren = $this->getExistingChildren($folderId);
426
+        $newChildren = $this->getNewChildren($path);
427
+
428
+        if ($this->useTransactions) {
429
+            \OC::$server->getDatabaseConnection()->beginTransaction();
430
+        }
431
+
432
+        $exceptionOccurred = false;
433
+        $childQueue = [];
434
+        foreach ($newChildren as $file) {
435
+            $child = $path ? $path . '/' . $file : $file;
436
+            try {
437
+                $existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : null;
438
+                $data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock);
439
+                if ($data) {
440
+                    if ($data['mimetype'] === 'httpd/unix-directory' and $recursive === self::SCAN_RECURSIVE) {
441
+                        $childQueue[$child] = $data['fileid'];
442
+                    } else if ($data['mimetype'] === 'httpd/unix-directory' and $recursive === self::SCAN_RECURSIVE_INCOMPLETE and $data['size'] === -1) {
443
+                        // only recurse into folders which aren't fully scanned
444
+                        $childQueue[$child] = $data['fileid'];
445
+                    } else if ($data['size'] === -1) {
446
+                        $size = -1;
447
+                    } else if ($size !== -1) {
448
+                        $size += $data['size'];
449
+                    }
450
+                }
451
+            } catch (\Doctrine\DBAL\DBALException $ex) {
452
+                // might happen if inserting duplicate while a scanning
453
+                // process is running in parallel
454
+                // log and ignore
455
+                if ($this->useTransactions) {
456
+                    \OC::$server->getDatabaseConnection()->rollback();
457
+                    \OC::$server->getDatabaseConnection()->beginTransaction();
458
+                }
459
+                \OC::$server->getLogger()->logException($ex, [
460
+                    'message' => 'Exception while scanning file "' . $child . '"',
461
+                    'level' => ILogger::DEBUG,
462
+                    'app' => 'core',
463
+                ]);
464
+                $exceptionOccurred = true;
465
+            } catch (\OCP\Lock\LockedException $e) {
466
+                if ($this->useTransactions) {
467
+                    \OC::$server->getDatabaseConnection()->rollback();
468
+                }
469
+                throw $e;
470
+            }
471
+        }
472
+        $removedChildren = \array_diff(array_keys($existingChildren), $newChildren);
473
+        foreach ($removedChildren as $childName) {
474
+            $child = $path ? $path . '/' . $childName : $childName;
475
+            $this->removeFromCache($child);
476
+        }
477
+        if ($this->useTransactions) {
478
+            \OC::$server->getDatabaseConnection()->commit();
479
+        }
480
+        if ($exceptionOccurred) {
481
+            // It might happen that the parallel scan process has already
482
+            // inserted mimetypes but those weren't available yet inside the transaction
483
+            // To make sure to have the updated mime types in such cases,
484
+            // we reload them here
485
+            \OC::$server->getMimeTypeLoader()->reset();
486
+        }
487
+        return $childQueue;
488
+    }
489
+
490
+    /**
491
+     * check if the file should be ignored when scanning
492
+     * NOTE: files with a '.part' extension are ignored as well!
493
+     *       prevents unfinished put requests to be scanned
494
+     *
495
+     * @param string $file
496
+     * @return boolean
497
+     */
498
+    public static function isPartialFile($file) {
499
+        if (pathinfo($file, PATHINFO_EXTENSION) === 'part') {
500
+            return true;
501
+        }
502
+        if (strpos($file, '.part/') !== false) {
503
+            return true;
504
+        }
505
+
506
+        return false;
507
+    }
508
+
509
+    /**
510
+     * walk over any folders that are not fully scanned yet and scan them
511
+     */
512
+    public function backgroundScan() {
513
+        if (!$this->cache->inCache('')) {
514
+            $this->runBackgroundScanJob(function () {
515
+                $this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
516
+            }, '');
517
+        } else {
518
+            $lastPath = null;
519
+            while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
520
+                $this->runBackgroundScanJob(function () use ($path) {
521
+                    $this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
522
+                }, $path);
523
+                // FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
524
+                // to make this possible
525
+                $lastPath = $path;
526
+            }
527
+        }
528
+    }
529
+
530
+    private function runBackgroundScanJob(callable $callback, $path) {
531
+        try {
532
+            $callback();
533
+            \OC_Hook::emit('Scanner', 'correctFolderSize', array('path' => $path));
534
+            if ($this->cacheActive && $this->cache instanceof Cache) {
535
+                $this->cache->correctFolderSize($path);
536
+            }
537
+        } catch (\OCP\Files\StorageInvalidException $e) {
538
+            // skip unavailable storages
539
+        } catch (\OCP\Files\StorageNotAvailableException $e) {
540
+            // skip unavailable storages
541
+        } catch (\OCP\Files\ForbiddenException $e) {
542
+            // skip forbidden storages
543
+        } catch (\OCP\Lock\LockedException $e) {
544
+            // skip unavailable storages
545
+        }
546
+    }
547
+
548
+    /**
549
+     * Set whether the cache is affected by scan operations
550
+     *
551
+     * @param boolean $active The active state of the cache
552
+     */
553
+    public function setCacheActive($active) {
554
+        $this->cacheActive = $active;
555
+    }
556 556
 }
Please login to merge, or discard this patch.
Spacing   +7 added lines, -7 removed lines patch added patch discarded remove patch
@@ -328,7 +328,7 @@  discard block
 block discarded – undo
328 328
 		}
329 329
 		if ($lock) {
330 330
 			if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
331
-				$this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
331
+				$this->storage->acquireLock('scanner::'.$path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
332 332
 				$this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
333 333
 			}
334 334
 		}
@@ -342,7 +342,7 @@  discard block
 block discarded – undo
342 342
 			if ($lock) {
343 343
 				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
344 344
 					$this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
345
-					$this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
345
+					$this->storage->releaseLock('scanner::'.$path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
346 346
 				}
347 347
 			}
348 348
 		}
@@ -432,7 +432,7 @@  discard block
 block discarded – undo
432 432
 		$exceptionOccurred = false;
433 433
 		$childQueue = [];
434 434
 		foreach ($newChildren as $file) {
435
-			$child = $path ? $path . '/' . $file : $file;
435
+			$child = $path ? $path.'/'.$file : $file;
436 436
 			try {
437 437
 				$existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : null;
438 438
 				$data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock);
@@ -457,7 +457,7 @@  discard block
 block discarded – undo
457 457
 					\OC::$server->getDatabaseConnection()->beginTransaction();
458 458
 				}
459 459
 				\OC::$server->getLogger()->logException($ex, [
460
-					'message' => 'Exception while scanning file "' . $child . '"',
460
+					'message' => 'Exception while scanning file "'.$child.'"',
461 461
 					'level' => ILogger::DEBUG,
462 462
 					'app' => 'core',
463 463
 				]);
@@ -471,7 +471,7 @@  discard block
 block discarded – undo
471 471
 		}
472 472
 		$removedChildren = \array_diff(array_keys($existingChildren), $newChildren);
473 473
 		foreach ($removedChildren as $childName) {
474
-			$child = $path ? $path . '/' . $childName : $childName;
474
+			$child = $path ? $path.'/'.$childName : $childName;
475 475
 			$this->removeFromCache($child);
476 476
 		}
477 477
 		if ($this->useTransactions) {
@@ -511,13 +511,13 @@  discard block
 block discarded – undo
511 511
 	 */
512 512
 	public function backgroundScan() {
513 513
 		if (!$this->cache->inCache('')) {
514
-			$this->runBackgroundScanJob(function () {
514
+			$this->runBackgroundScanJob(function() {
515 515
 				$this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
516 516
 			}, '');
517 517
 		} else {
518 518
 			$lastPath = null;
519 519
 			while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
520
-				$this->runBackgroundScanJob(function () use ($path) {
520
+				$this->runBackgroundScanJob(function() use ($path) {
521 521
 					$this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
522 522
 				}, $path);
523 523
 				// FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
Please login to merge, or discard this patch.