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