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