Passed
Push — master ( 7042f9...a688f4 )
by Roeland
21:39 queued 10:37
created
lib/private/Files/Cache/Scanner.php 2 patches
Indentation   +501 added lines, -501 removed lines patch added patch discarded remove patch
@@ -55,505 +55,505 @@
 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
-	 * @return array an array of metadata of the scanned file
130
-	 * @throws \OC\ServerNotAvailableException
131
-	 * @throws \OCP\Lock\LockedException
132
-	 */
133
-	public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true) {
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 = $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
-
242
-				} else {
243
-					$this->removeFromCache($file);
244
-				}
245
-			} catch (\Exception $e) {
246
-				if ($lock) {
247
-					if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
248
-						$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
249
-					}
250
-				}
251
-				throw $e;
252
-			}
253
-
254
-			//release the acquired lock
255
-			if ($lock) {
256
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
257
-					$this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
258
-				}
259
-			}
260
-
261
-			if ($data && !isset($data['encrypted'])) {
262
-				$data['encrypted'] = false;
263
-			}
264
-			return $data;
265
-		}
266
-
267
-		return null;
268
-	}
269
-
270
-	protected function removeFromCache($path) {
271
-		\OC_Hook::emit('Scanner', 'removeFromCache', ['file' => $path]);
272
-		$this->emit('\OC\Files\Cache\Scanner', 'removeFromCache', [$path]);
273
-		if ($this->cacheActive) {
274
-			$this->cache->remove($path);
275
-		}
276
-	}
277
-
278
-	/**
279
-	 * @param string $path
280
-	 * @param array $data
281
-	 * @param int $fileId
282
-	 * @return int the id of the added file
283
-	 */
284
-	protected function addToCache($path, $data, $fileId = -1) {
285
-		if (isset($data['scan_permissions'])) {
286
-			$data['permissions'] = $data['scan_permissions'];
287
-		}
288
-		\OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
289
-		$this->emit('\OC\Files\Cache\Scanner', 'addToCache', [$path, $this->storageId, $data]);
290
-		if ($this->cacheActive) {
291
-			if ($fileId !== -1) {
292
-				$this->cache->update($fileId, $data);
293
-				return $fileId;
294
-			} else {
295
-				return $this->cache->insert($path, $data);
296
-			}
297
-		} else {
298
-			return -1;
299
-		}
300
-	}
301
-
302
-	/**
303
-	 * @param string $path
304
-	 * @param array $data
305
-	 * @param int $fileId
306
-	 */
307
-	protected function updateCache($path, $data, $fileId = -1) {
308
-		\OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
309
-		$this->emit('\OC\Files\Cache\Scanner', 'updateCache', [$path, $this->storageId, $data]);
310
-		if ($this->cacheActive) {
311
-			if ($fileId !== -1) {
312
-				$this->cache->update($fileId, $data);
313
-			} else {
314
-				$this->cache->put($path, $data);
315
-			}
316
-		}
317
-	}
318
-
319
-	/**
320
-	 * scan a folder and all it's children
321
-	 *
322
-	 * @param string $path
323
-	 * @param bool $recursive
324
-	 * @param int $reuse
325
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
326
-	 * @return array an array of the meta data of the scanned file or folder
327
-	 */
328
-	public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
329
-		if ($reuse === -1) {
330
-			$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
331
-		}
332
-		if ($lock) {
333
-			if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
334
-				$this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
335
-				$this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
336
-			}
337
-		}
338
-		try {
339
-			$data = $this->scanFile($path, $reuse, -1, null, $lock);
340
-			if ($data and $data['mimetype'] === 'httpd/unix-directory') {
341
-				$size = $this->scanChildren($path, $recursive, $reuse, $data['fileid'], $lock);
342
-				$data['size'] = $size;
343
-			}
344
-		} finally {
345
-			if ($lock) {
346
-				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
347
-					$this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
348
-					$this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
349
-				}
350
-			}
351
-		}
352
-		return $data;
353
-	}
354
-
355
-	/**
356
-	 * Get the children currently in the cache
357
-	 *
358
-	 * @param int $folderId
359
-	 * @return array[]
360
-	 */
361
-	protected function getExistingChildren($folderId) {
362
-		$existingChildren = [];
363
-		$children = $this->cache->getFolderContentsById($folderId);
364
-		foreach ($children as $child) {
365
-			$existingChildren[$child['name']] = $child;
366
-		}
367
-		return $existingChildren;
368
-	}
369
-
370
-	/**
371
-	 * Get the children from the storage
372
-	 *
373
-	 * @param string $folder
374
-	 * @return string[]
375
-	 */
376
-	protected function getNewChildren($folder) {
377
-		$children = [];
378
-		if ($dh = $this->storage->opendir($folder)) {
379
-			if (is_resource($dh)) {
380
-				while (($file = readdir($dh)) !== false) {
381
-					if (!Filesystem::isIgnoredDir($file)) {
382
-						$children[] = trim(\OC\Files\Filesystem::normalizePath($file), '/');
383
-					}
384
-				}
385
-			}
386
-		}
387
-		return $children;
388
-	}
389
-
390
-	/**
391
-	 * scan all the files and folders in a folder
392
-	 *
393
-	 * @param string $path
394
-	 * @param bool $recursive
395
-	 * @param int $reuse
396
-	 * @param int $folderId id for the folder to be scanned
397
-	 * @param bool $lock set to false to disable getting an additional read lock during scanning
398
-	 * @return int the size of the scanned folder or -1 if the size is unknown at this stage
399
-	 */
400
-	protected function scanChildren($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $folderId = null, $lock = true) {
401
-		if ($reuse === -1) {
402
-			$reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
403
-		}
404
-		$this->emit('\OC\Files\Cache\Scanner', 'scanFolder', [$path, $this->storageId]);
405
-		$size = 0;
406
-		if (!is_null($folderId)) {
407
-			$folderId = $this->cache->getId($path);
408
-		}
409
-		$childQueue = $this->handleChildren($path, $recursive, $reuse, $folderId, $lock, $size);
410
-
411
-		foreach ($childQueue as $child => $childId) {
412
-			$childSize = $this->scanChildren($child, $recursive, $reuse, $childId, $lock);
413
-			if ($childSize === -1) {
414
-				$size = -1;
415
-			} else if ($size !== -1) {
416
-				$size += $childSize;
417
-			}
418
-		}
419
-		if ($this->cacheActive) {
420
-			$this->cache->update($folderId, ['size' => $size]);
421
-		}
422
-		$this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', [$path, $this->storageId]);
423
-		return $size;
424
-	}
425
-
426
-	private function handleChildren($path, $recursive, $reuse, $folderId, $lock, &$size) {
427
-		// we put this in it's own function so it cleans up the memory before we start recursing
428
-		$existingChildren = $this->getExistingChildren($folderId);
429
-		$newChildren = $this->getNewChildren($path);
430
-
431
-		if ($this->useTransactions) {
432
-			\OC::$server->getDatabaseConnection()->beginTransaction();
433
-		}
434
-
435
-		$exceptionOccurred = false;
436
-		$childQueue = [];
437
-		foreach ($newChildren as $file) {
438
-			$child = $path ? $path . '/' . $file : $file;
439
-			try {
440
-				$existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : false;
441
-				$data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock);
442
-				if ($data) {
443
-					if ($data['mimetype'] === 'httpd/unix-directory' and $recursive === self::SCAN_RECURSIVE) {
444
-						$childQueue[$child] = $data['fileid'];
445
-					} else if ($data['mimetype'] === 'httpd/unix-directory' and $recursive === self::SCAN_RECURSIVE_INCOMPLETE and $data['size'] === -1) {
446
-						// only recurse into folders which aren't fully scanned
447
-						$childQueue[$child] = $data['fileid'];
448
-					} else if ($data['size'] === -1) {
449
-						$size = -1;
450
-					} else if ($size !== -1) {
451
-						$size += $data['size'];
452
-					}
453
-				}
454
-			} catch (\Doctrine\DBAL\DBALException $ex) {
455
-				// might happen if inserting duplicate while a scanning
456
-				// process is running in parallel
457
-				// log and ignore
458
-				if ($this->useTransactions) {
459
-					\OC::$server->getDatabaseConnection()->rollback();
460
-					\OC::$server->getDatabaseConnection()->beginTransaction();
461
-				}
462
-				\OC::$server->getLogger()->logException($ex, [
463
-					'message' => 'Exception while scanning file "' . $child . '"',
464
-					'level' => ILogger::DEBUG,
465
-					'app' => 'core',
466
-				]);
467
-				$exceptionOccurred = true;
468
-			} catch (\OCP\Lock\LockedException $e) {
469
-				if ($this->useTransactions) {
470
-					\OC::$server->getDatabaseConnection()->rollback();
471
-				}
472
-				throw $e;
473
-			}
474
-		}
475
-		$removedChildren = \array_diff(array_keys($existingChildren), $newChildren);
476
-		foreach ($removedChildren as $childName) {
477
-			$child = $path ? $path . '/' . $childName : $childName;
478
-			$this->removeFromCache($child);
479
-		}
480
-		if ($this->useTransactions) {
481
-			\OC::$server->getDatabaseConnection()->commit();
482
-		}
483
-		if ($exceptionOccurred) {
484
-			// It might happen that the parallel scan process has already
485
-			// inserted mimetypes but those weren't available yet inside the transaction
486
-			// To make sure to have the updated mime types in such cases,
487
-			// we reload them here
488
-			\OC::$server->getMimeTypeLoader()->reset();
489
-		}
490
-		return $childQueue;
491
-	}
492
-
493
-	/**
494
-	 * check if the file should be ignored when scanning
495
-	 * NOTE: files with a '.part' extension are ignored as well!
496
-	 *       prevents unfinished put requests to be scanned
497
-	 *
498
-	 * @param string $file
499
-	 * @return boolean
500
-	 */
501
-	public static function isPartialFile($file) {
502
-		if (pathinfo($file, PATHINFO_EXTENSION) === 'part') {
503
-			return true;
504
-		}
505
-		if (strpos($file, '.part/') !== false) {
506
-			return true;
507
-		}
508
-
509
-		return false;
510
-	}
511
-
512
-	/**
513
-	 * walk over any folders that are not fully scanned yet and scan them
514
-	 */
515
-	public function backgroundScan() {
516
-		if (!$this->cache->inCache('')) {
517
-			$this->runBackgroundScanJob(function () {
518
-				$this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
519
-			}, '');
520
-		} else {
521
-			$lastPath = null;
522
-			while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
523
-				$this->runBackgroundScanJob(function () use ($path) {
524
-					$this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
525
-				}, $path);
526
-				// FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
527
-				// to make this possible
528
-				$lastPath = $path;
529
-			}
530
-		}
531
-	}
532
-
533
-	private function runBackgroundScanJob(callable $callback, $path) {
534
-		try {
535
-			$callback();
536
-			\OC_Hook::emit('Scanner', 'correctFolderSize', ['path' => $path]);
537
-			if ($this->cacheActive && $this->cache instanceof Cache) {
538
-				$this->cache->correctFolderSize($path, null, true);
539
-			}
540
-		} catch (\OCP\Files\StorageInvalidException $e) {
541
-			// skip unavailable storages
542
-		} catch (\OCP\Files\StorageNotAvailableException $e) {
543
-			// skip unavailable storages
544
-		} catch (\OCP\Files\ForbiddenException $e) {
545
-			// skip forbidden storages
546
-		} catch (\OCP\Lock\LockedException $e) {
547
-			// skip unavailable storages
548
-		}
549
-	}
550
-
551
-	/**
552
-	 * Set whether the cache is affected by scan operations
553
-	 *
554
-	 * @param boolean $active The active state of the cache
555
-	 */
556
-	public function setCacheActive($active) {
557
-		$this->cacheActive = $active;
558
-	}
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
+     * @return array an array of metadata of the scanned file
130
+     * @throws \OC\ServerNotAvailableException
131
+     * @throws \OCP\Lock\LockedException
132
+     */
133
+    public function scanFile($file, $reuseExisting = 0, $parentId = -1, $cacheData = null, $lock = true) {
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 = $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
+
242
+                } else {
243
+                    $this->removeFromCache($file);
244
+                }
245
+            } catch (\Exception $e) {
246
+                if ($lock) {
247
+                    if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
248
+                        $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
249
+                    }
250
+                }
251
+                throw $e;
252
+            }
253
+
254
+            //release the acquired lock
255
+            if ($lock) {
256
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
257
+                    $this->storage->releaseLock($file, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
258
+                }
259
+            }
260
+
261
+            if ($data && !isset($data['encrypted'])) {
262
+                $data['encrypted'] = false;
263
+            }
264
+            return $data;
265
+        }
266
+
267
+        return null;
268
+    }
269
+
270
+    protected function removeFromCache($path) {
271
+        \OC_Hook::emit('Scanner', 'removeFromCache', ['file' => $path]);
272
+        $this->emit('\OC\Files\Cache\Scanner', 'removeFromCache', [$path]);
273
+        if ($this->cacheActive) {
274
+            $this->cache->remove($path);
275
+        }
276
+    }
277
+
278
+    /**
279
+     * @param string $path
280
+     * @param array $data
281
+     * @param int $fileId
282
+     * @return int the id of the added file
283
+     */
284
+    protected function addToCache($path, $data, $fileId = -1) {
285
+        if (isset($data['scan_permissions'])) {
286
+            $data['permissions'] = $data['scan_permissions'];
287
+        }
288
+        \OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
289
+        $this->emit('\OC\Files\Cache\Scanner', 'addToCache', [$path, $this->storageId, $data]);
290
+        if ($this->cacheActive) {
291
+            if ($fileId !== -1) {
292
+                $this->cache->update($fileId, $data);
293
+                return $fileId;
294
+            } else {
295
+                return $this->cache->insert($path, $data);
296
+            }
297
+        } else {
298
+            return -1;
299
+        }
300
+    }
301
+
302
+    /**
303
+     * @param string $path
304
+     * @param array $data
305
+     * @param int $fileId
306
+     */
307
+    protected function updateCache($path, $data, $fileId = -1) {
308
+        \OC_Hook::emit('Scanner', 'addToCache', ['file' => $path, 'data' => $data]);
309
+        $this->emit('\OC\Files\Cache\Scanner', 'updateCache', [$path, $this->storageId, $data]);
310
+        if ($this->cacheActive) {
311
+            if ($fileId !== -1) {
312
+                $this->cache->update($fileId, $data);
313
+            } else {
314
+                $this->cache->put($path, $data);
315
+            }
316
+        }
317
+    }
318
+
319
+    /**
320
+     * scan a folder and all it's children
321
+     *
322
+     * @param string $path
323
+     * @param bool $recursive
324
+     * @param int $reuse
325
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
326
+     * @return array an array of the meta data of the scanned file or folder
327
+     */
328
+    public function scan($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $lock = true) {
329
+        if ($reuse === -1) {
330
+            $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
331
+        }
332
+        if ($lock) {
333
+            if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
334
+                $this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
335
+                $this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
336
+            }
337
+        }
338
+        try {
339
+            $data = $this->scanFile($path, $reuse, -1, null, $lock);
340
+            if ($data and $data['mimetype'] === 'httpd/unix-directory') {
341
+                $size = $this->scanChildren($path, $recursive, $reuse, $data['fileid'], $lock);
342
+                $data['size'] = $size;
343
+            }
344
+        } finally {
345
+            if ($lock) {
346
+                if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
347
+                    $this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
348
+                    $this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
349
+                }
350
+            }
351
+        }
352
+        return $data;
353
+    }
354
+
355
+    /**
356
+     * Get the children currently in the cache
357
+     *
358
+     * @param int $folderId
359
+     * @return array[]
360
+     */
361
+    protected function getExistingChildren($folderId) {
362
+        $existingChildren = [];
363
+        $children = $this->cache->getFolderContentsById($folderId);
364
+        foreach ($children as $child) {
365
+            $existingChildren[$child['name']] = $child;
366
+        }
367
+        return $existingChildren;
368
+    }
369
+
370
+    /**
371
+     * Get the children from the storage
372
+     *
373
+     * @param string $folder
374
+     * @return string[]
375
+     */
376
+    protected function getNewChildren($folder) {
377
+        $children = [];
378
+        if ($dh = $this->storage->opendir($folder)) {
379
+            if (is_resource($dh)) {
380
+                while (($file = readdir($dh)) !== false) {
381
+                    if (!Filesystem::isIgnoredDir($file)) {
382
+                        $children[] = trim(\OC\Files\Filesystem::normalizePath($file), '/');
383
+                    }
384
+                }
385
+            }
386
+        }
387
+        return $children;
388
+    }
389
+
390
+    /**
391
+     * scan all the files and folders in a folder
392
+     *
393
+     * @param string $path
394
+     * @param bool $recursive
395
+     * @param int $reuse
396
+     * @param int $folderId id for the folder to be scanned
397
+     * @param bool $lock set to false to disable getting an additional read lock during scanning
398
+     * @return int the size of the scanned folder or -1 if the size is unknown at this stage
399
+     */
400
+    protected function scanChildren($path, $recursive = self::SCAN_RECURSIVE, $reuse = -1, $folderId = null, $lock = true) {
401
+        if ($reuse === -1) {
402
+            $reuse = ($recursive === self::SCAN_SHALLOW) ? self::REUSE_ETAG | self::REUSE_SIZE : self::REUSE_ETAG;
403
+        }
404
+        $this->emit('\OC\Files\Cache\Scanner', 'scanFolder', [$path, $this->storageId]);
405
+        $size = 0;
406
+        if (!is_null($folderId)) {
407
+            $folderId = $this->cache->getId($path);
408
+        }
409
+        $childQueue = $this->handleChildren($path, $recursive, $reuse, $folderId, $lock, $size);
410
+
411
+        foreach ($childQueue as $child => $childId) {
412
+            $childSize = $this->scanChildren($child, $recursive, $reuse, $childId, $lock);
413
+            if ($childSize === -1) {
414
+                $size = -1;
415
+            } else if ($size !== -1) {
416
+                $size += $childSize;
417
+            }
418
+        }
419
+        if ($this->cacheActive) {
420
+            $this->cache->update($folderId, ['size' => $size]);
421
+        }
422
+        $this->emit('\OC\Files\Cache\Scanner', 'postScanFolder', [$path, $this->storageId]);
423
+        return $size;
424
+    }
425
+
426
+    private function handleChildren($path, $recursive, $reuse, $folderId, $lock, &$size) {
427
+        // we put this in it's own function so it cleans up the memory before we start recursing
428
+        $existingChildren = $this->getExistingChildren($folderId);
429
+        $newChildren = $this->getNewChildren($path);
430
+
431
+        if ($this->useTransactions) {
432
+            \OC::$server->getDatabaseConnection()->beginTransaction();
433
+        }
434
+
435
+        $exceptionOccurred = false;
436
+        $childQueue = [];
437
+        foreach ($newChildren as $file) {
438
+            $child = $path ? $path . '/' . $file : $file;
439
+            try {
440
+                $existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : false;
441
+                $data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock);
442
+                if ($data) {
443
+                    if ($data['mimetype'] === 'httpd/unix-directory' and $recursive === self::SCAN_RECURSIVE) {
444
+                        $childQueue[$child] = $data['fileid'];
445
+                    } else if ($data['mimetype'] === 'httpd/unix-directory' and $recursive === self::SCAN_RECURSIVE_INCOMPLETE and $data['size'] === -1) {
446
+                        // only recurse into folders which aren't fully scanned
447
+                        $childQueue[$child] = $data['fileid'];
448
+                    } else if ($data['size'] === -1) {
449
+                        $size = -1;
450
+                    } else if ($size !== -1) {
451
+                        $size += $data['size'];
452
+                    }
453
+                }
454
+            } catch (\Doctrine\DBAL\DBALException $ex) {
455
+                // might happen if inserting duplicate while a scanning
456
+                // process is running in parallel
457
+                // log and ignore
458
+                if ($this->useTransactions) {
459
+                    \OC::$server->getDatabaseConnection()->rollback();
460
+                    \OC::$server->getDatabaseConnection()->beginTransaction();
461
+                }
462
+                \OC::$server->getLogger()->logException($ex, [
463
+                    'message' => 'Exception while scanning file "' . $child . '"',
464
+                    'level' => ILogger::DEBUG,
465
+                    'app' => 'core',
466
+                ]);
467
+                $exceptionOccurred = true;
468
+            } catch (\OCP\Lock\LockedException $e) {
469
+                if ($this->useTransactions) {
470
+                    \OC::$server->getDatabaseConnection()->rollback();
471
+                }
472
+                throw $e;
473
+            }
474
+        }
475
+        $removedChildren = \array_diff(array_keys($existingChildren), $newChildren);
476
+        foreach ($removedChildren as $childName) {
477
+            $child = $path ? $path . '/' . $childName : $childName;
478
+            $this->removeFromCache($child);
479
+        }
480
+        if ($this->useTransactions) {
481
+            \OC::$server->getDatabaseConnection()->commit();
482
+        }
483
+        if ($exceptionOccurred) {
484
+            // It might happen that the parallel scan process has already
485
+            // inserted mimetypes but those weren't available yet inside the transaction
486
+            // To make sure to have the updated mime types in such cases,
487
+            // we reload them here
488
+            \OC::$server->getMimeTypeLoader()->reset();
489
+        }
490
+        return $childQueue;
491
+    }
492
+
493
+    /**
494
+     * check if the file should be ignored when scanning
495
+     * NOTE: files with a '.part' extension are ignored as well!
496
+     *       prevents unfinished put requests to be scanned
497
+     *
498
+     * @param string $file
499
+     * @return boolean
500
+     */
501
+    public static function isPartialFile($file) {
502
+        if (pathinfo($file, PATHINFO_EXTENSION) === 'part') {
503
+            return true;
504
+        }
505
+        if (strpos($file, '.part/') !== false) {
506
+            return true;
507
+        }
508
+
509
+        return false;
510
+    }
511
+
512
+    /**
513
+     * walk over any folders that are not fully scanned yet and scan them
514
+     */
515
+    public function backgroundScan() {
516
+        if (!$this->cache->inCache('')) {
517
+            $this->runBackgroundScanJob(function () {
518
+                $this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
519
+            }, '');
520
+        } else {
521
+            $lastPath = null;
522
+            while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
523
+                $this->runBackgroundScanJob(function () use ($path) {
524
+                    $this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
525
+                }, $path);
526
+                // FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
527
+                // to make this possible
528
+                $lastPath = $path;
529
+            }
530
+        }
531
+    }
532
+
533
+    private function runBackgroundScanJob(callable $callback, $path) {
534
+        try {
535
+            $callback();
536
+            \OC_Hook::emit('Scanner', 'correctFolderSize', ['path' => $path]);
537
+            if ($this->cacheActive && $this->cache instanceof Cache) {
538
+                $this->cache->correctFolderSize($path, null, true);
539
+            }
540
+        } catch (\OCP\Files\StorageInvalidException $e) {
541
+            // skip unavailable storages
542
+        } catch (\OCP\Files\StorageNotAvailableException $e) {
543
+            // skip unavailable storages
544
+        } catch (\OCP\Files\ForbiddenException $e) {
545
+            // skip forbidden storages
546
+        } catch (\OCP\Lock\LockedException $e) {
547
+            // skip unavailable storages
548
+        }
549
+    }
550
+
551
+    /**
552
+     * Set whether the cache is affected by scan operations
553
+     *
554
+     * @param boolean $active The active state of the cache
555
+     */
556
+    public function setCacheActive($active) {
557
+        $this->cacheActive = $active;
558
+    }
559 559
 }
Please login to merge, or discard this patch.
Spacing   +7 added lines, -7 removed lines patch added patch discarded remove patch
@@ -331,7 +331,7 @@  discard block
 block discarded – undo
331 331
 		}
332 332
 		if ($lock) {
333 333
 			if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
334
-				$this->storage->acquireLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
334
+				$this->storage->acquireLock('scanner::'.$path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
335 335
 				$this->storage->acquireLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
336 336
 			}
337 337
 		}
@@ -345,7 +345,7 @@  discard block
 block discarded – undo
345 345
 			if ($lock) {
346 346
 				if ($this->storage->instanceOfStorage('\OCP\Files\Storage\ILockingStorage')) {
347 347
 					$this->storage->releaseLock($path, ILockingProvider::LOCK_SHARED, $this->lockingProvider);
348
-					$this->storage->releaseLock('scanner::' . $path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
348
+					$this->storage->releaseLock('scanner::'.$path, ILockingProvider::LOCK_EXCLUSIVE, $this->lockingProvider);
349 349
 				}
350 350
 			}
351 351
 		}
@@ -435,7 +435,7 @@  discard block
 block discarded – undo
435 435
 		$exceptionOccurred = false;
436 436
 		$childQueue = [];
437 437
 		foreach ($newChildren as $file) {
438
-			$child = $path ? $path . '/' . $file : $file;
438
+			$child = $path ? $path.'/'.$file : $file;
439 439
 			try {
440 440
 				$existingData = isset($existingChildren[$file]) ? $existingChildren[$file] : false;
441 441
 				$data = $this->scanFile($child, $reuse, $folderId, $existingData, $lock);
@@ -460,7 +460,7 @@  discard block
 block discarded – undo
460 460
 					\OC::$server->getDatabaseConnection()->beginTransaction();
461 461
 				}
462 462
 				\OC::$server->getLogger()->logException($ex, [
463
-					'message' => 'Exception while scanning file "' . $child . '"',
463
+					'message' => 'Exception while scanning file "'.$child.'"',
464 464
 					'level' => ILogger::DEBUG,
465 465
 					'app' => 'core',
466 466
 				]);
@@ -474,7 +474,7 @@  discard block
 block discarded – undo
474 474
 		}
475 475
 		$removedChildren = \array_diff(array_keys($existingChildren), $newChildren);
476 476
 		foreach ($removedChildren as $childName) {
477
-			$child = $path ? $path . '/' . $childName : $childName;
477
+			$child = $path ? $path.'/'.$childName : $childName;
478 478
 			$this->removeFromCache($child);
479 479
 		}
480 480
 		if ($this->useTransactions) {
@@ -514,13 +514,13 @@  discard block
 block discarded – undo
514 514
 	 */
515 515
 	public function backgroundScan() {
516 516
 		if (!$this->cache->inCache('')) {
517
-			$this->runBackgroundScanJob(function () {
517
+			$this->runBackgroundScanJob(function() {
518 518
 				$this->scan('', self::SCAN_RECURSIVE, self::REUSE_ETAG);
519 519
 			}, '');
520 520
 		} else {
521 521
 			$lastPath = null;
522 522
 			while (($path = $this->cache->getIncomplete()) !== false && $path !== $lastPath) {
523
-				$this->runBackgroundScanJob(function () use ($path) {
523
+				$this->runBackgroundScanJob(function() use ($path) {
524 524
 					$this->scan($path, self::SCAN_RECURSIVE_INCOMPLETE, self::REUSE_ETAG | self::REUSE_SIZE);
525 525
 				}, $path);
526 526
 				// FIXME: this won't proceed with the next item, needs revamping of getIncomplete()
Please login to merge, or discard this patch.
lib/private/Files/Cache/Cache.php 2 patches
Indentation   +932 added lines, -932 removed lines patch added patch discarded remove patch
@@ -61,936 +61,936 @@
 block discarded – undo
61 61
  * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
62 62
  */
63 63
 class Cache implements ICache {
64
-	use MoveFromCacheTrait {
65
-		MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
66
-	}
67
-
68
-	/**
69
-	 * @var array partial data for the cache
70
-	 */
71
-	protected $partial = [];
72
-
73
-	/**
74
-	 * @var string
75
-	 */
76
-	protected $storageId;
77
-
78
-	private $storage;
79
-
80
-	/**
81
-	 * @var Storage $storageCache
82
-	 */
83
-	protected $storageCache;
84
-
85
-	/** @var IMimeTypeLoader */
86
-	protected $mimetypeLoader;
87
-
88
-	/**
89
-	 * @var IDBConnection
90
-	 */
91
-	protected $connection;
92
-
93
-	protected $eventDispatcher;
94
-
95
-	/** @var QuerySearchHelper */
96
-	protected $querySearchHelper;
97
-
98
-	/**
99
-	 * @param IStorage $storage
100
-	 */
101
-	public function __construct(IStorage $storage) {
102
-		$this->storageId = $storage->getId();
103
-		$this->storage = $storage;
104
-		if (strlen($this->storageId) > 64) {
105
-			$this->storageId = md5($this->storageId);
106
-		}
107
-
108
-		$this->storageCache = new Storage($storage);
109
-		$this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
110
-		$this->connection = \OC::$server->getDatabaseConnection();
111
-		$this->eventDispatcher = \OC::$server->getEventDispatcher();
112
-		$this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
113
-	}
114
-
115
-	private function getQueryBuilder() {
116
-		return new CacheQueryBuilder(
117
-			$this->connection,
118
-			\OC::$server->getSystemConfig(),
119
-			\OC::$server->getLogger(),
120
-			$this
121
-		);
122
-	}
123
-
124
-	/**
125
-	 * Get the numeric storage id for this cache's storage
126
-	 *
127
-	 * @return int
128
-	 */
129
-	public function getNumericStorageId() {
130
-		return $this->storageCache->getNumericId();
131
-	}
132
-
133
-	/**
134
-	 * get the stored metadata of a file or folder
135
-	 *
136
-	 * @param string | int $file either the path of a file or folder or the file id for a file or folder
137
-	 * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
138
-	 */
139
-	public function get($file) {
140
-		$query = $this->getQueryBuilder();
141
-		$query->selectFileCache();
142
-
143
-		if (is_string($file) or $file == '') {
144
-			// normalize file
145
-			$file = $this->normalize($file);
146
-
147
-			$query->whereStorageId()
148
-				->wherePath($file);
149
-		} else { //file id
150
-			$query->whereFileId($file);
151
-		}
152
-
153
-		$data = $query->execute()->fetch();
154
-
155
-		//merge partial data
156
-		if (!$data and is_string($file) and isset($this->partial[$file])) {
157
-			return $this->partial[$file];
158
-		} else if (!$data) {
159
-			return $data;
160
-		} else {
161
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
162
-		}
163
-	}
164
-
165
-	/**
166
-	 * Create a CacheEntry from database row
167
-	 *
168
-	 * @param array $data
169
-	 * @param IMimeTypeLoader $mimetypeLoader
170
-	 * @return CacheEntry
171
-	 */
172
-	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
173
-		//fix types
174
-		$data['fileid'] = (int)$data['fileid'];
175
-		$data['parent'] = (int)$data['parent'];
176
-		$data['size'] = 0 + $data['size'];
177
-		$data['mtime'] = (int)$data['mtime'];
178
-		$data['storage_mtime'] = (int)$data['storage_mtime'];
179
-		$data['encryptedVersion'] = (int)$data['encrypted'];
180
-		$data['encrypted'] = (bool)$data['encrypted'];
181
-		$data['storage_id'] = $data['storage'];
182
-		$data['storage'] = (int)$data['storage'];
183
-		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
184
-		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
185
-		if ($data['storage_mtime'] == 0) {
186
-			$data['storage_mtime'] = $data['mtime'];
187
-		}
188
-		$data['permissions'] = (int)$data['permissions'];
189
-		if (isset($data['creation_time'])) {
190
-			$data['creation_time'] = (int) $data['creation_time'];
191
-		}
192
-		if (isset($data['upload_time'])) {
193
-			$data['upload_time'] = (int) $data['upload_time'];
194
-		}
195
-		return new CacheEntry($data);
196
-	}
197
-
198
-	/**
199
-	 * get the metadata of all files stored in $folder
200
-	 *
201
-	 * @param string $folder
202
-	 * @return ICacheEntry[]
203
-	 */
204
-	public function getFolderContents($folder) {
205
-		$fileId = $this->getId($folder);
206
-		return $this->getFolderContentsById($fileId);
207
-	}
208
-
209
-	/**
210
-	 * get the metadata of all files stored in $folder
211
-	 *
212
-	 * @param int $fileId the file id of the folder
213
-	 * @return ICacheEntry[]
214
-	 */
215
-	public function getFolderContentsById($fileId) {
216
-		if ($fileId > -1) {
217
-			$query = $this->getQueryBuilder();
218
-			$query->selectFileCache()
219
-				->whereParent($fileId)
220
-				->orderBy('name', 'ASC');
221
-
222
-			$files = $query->execute()->fetchAll();
223
-			return array_map(function (array $data) {
224
-				return self::cacheEntryFromData($data, $this->mimetypeLoader);
225
-			}, $files);
226
-		}
227
-		return [];
228
-	}
229
-
230
-	/**
231
-	 * insert or update meta data for a file or folder
232
-	 *
233
-	 * @param string $file
234
-	 * @param array $data
235
-	 *
236
-	 * @return int file id
237
-	 * @throws \RuntimeException
238
-	 */
239
-	public function put($file, array $data) {
240
-		if (($id = $this->getId($file)) > -1) {
241
-			$this->update($id, $data);
242
-			return $id;
243
-		} else {
244
-			return $this->insert($file, $data);
245
-		}
246
-	}
247
-
248
-	/**
249
-	 * insert meta data for a new file or folder
250
-	 *
251
-	 * @param string $file
252
-	 * @param array $data
253
-	 *
254
-	 * @return int file id
255
-	 * @throws \RuntimeException
256
-	 *
257
-	 * @suppress SqlInjectionChecker
258
-	 */
259
-	public function insert($file, array $data) {
260
-		// normalize file
261
-		$file = $this->normalize($file);
262
-
263
-		if (isset($this->partial[$file])) { //add any saved partial data
264
-			$data = array_merge($this->partial[$file], $data);
265
-			unset($this->partial[$file]);
266
-		}
267
-
268
-		$requiredFields = ['size', 'mtime', 'mimetype'];
269
-		foreach ($requiredFields as $field) {
270
-			if (!isset($data[$field])) { //data not complete save as partial and return
271
-				$this->partial[$file] = $data;
272
-				return -1;
273
-			}
274
-		}
275
-
276
-		$data['path'] = $file;
277
-		if (!isset($data['parent'])) {
278
-			$data['parent'] = $this->getParentId($file);
279
-		}
280
-		$data['name'] = basename($file);
281
-
282
-		[$values, $extensionValues] = $this->normalizeData($data);
283
-		$values['storage'] = $this->getNumericStorageId();
284
-
285
-		try {
286
-			$builder = $this->connection->getQueryBuilder();
287
-			$builder->insert('filecache');
288
-
289
-			foreach ($values as $column => $value) {
290
-				$builder->setValue($column, $builder->createNamedParameter($value));
291
-			}
292
-
293
-			if ($builder->execute()) {
294
-				$fileId = $builder->getLastInsertId();
295
-
296
-				if (count($extensionValues)) {
297
-					$query = $this->getQueryBuilder();
298
-					$query->insert('filecache_extended');
299
-
300
-					$query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
301
-					foreach ($extensionValues as $column => $value) {
302
-						$query->setValue($column, $query->createNamedParameter($value));
303
-					}
304
-					$query->execute();
305
-				}
306
-
307
-				$this->eventDispatcher->dispatch(CacheInsertEvent::class, new CacheInsertEvent($this->storage, $file, $fileId));
308
-				return $fileId;
309
-			}
310
-		} catch (UniqueConstraintViolationException $e) {
311
-			// entry exists already
312
-			if ($this->connection->inTransaction()) {
313
-				$this->connection->commit();
314
-				$this->connection->beginTransaction();
315
-			}
316
-		}
317
-
318
-		// The file was created in the mean time
319
-		if (($id = $this->getId($file)) > -1) {
320
-			$this->update($id, $data);
321
-			return $id;
322
-		} else {
323
-			throw new \RuntimeException('File entry could not be inserted but could also not be selected with getId() in order to perform an update. Please try again.');
324
-		}
325
-	}
326
-
327
-	/**
328
-	 * update the metadata of an existing file or folder in the cache
329
-	 *
330
-	 * @param int $id the fileid of the existing file or folder
331
-	 * @param array $data [$key => $value] the metadata to update, only the fields provided in the array will be updated, non-provided values will remain unchanged
332
-	 */
333
-	public function update($id, array $data) {
334
-
335
-		if (isset($data['path'])) {
336
-			// normalize path
337
-			$data['path'] = $this->normalize($data['path']);
338
-		}
339
-
340
-		if (isset($data['name'])) {
341
-			// normalize path
342
-			$data['name'] = $this->normalize($data['name']);
343
-		}
344
-
345
-		[$values, $extensionValues] = $this->normalizeData($data);
346
-
347
-		if (count($values)) {
348
-			$query = $this->getQueryBuilder();
349
-
350
-			$query->update('filecache')
351
-				->whereFileId($id)
352
-				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
353
-					return $query->expr()->orX(
354
-						$query->expr()->neq($key, $query->createNamedParameter($value)),
355
-						$query->expr()->isNull($key)
356
-					);
357
-				}, array_keys($values), array_values($values))));
358
-
359
-			foreach ($values as $key => $value) {
360
-				$query->set($key, $query->createNamedParameter($value));
361
-			}
362
-
363
-			$query->execute();
364
-		}
365
-
366
-		if (count($extensionValues)) {
367
-			try {
368
-				$query = $this->getQueryBuilder();
369
-				$query->insert('filecache_extended');
370
-
371
-				$query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
372
-				foreach ($extensionValues as $column => $value) {
373
-					$query->setValue($column, $query->createNamedParameter($value));
374
-				}
375
-
376
-				$query->execute();
377
-			} catch (UniqueConstraintViolationException $e) {
378
-				$query = $this->getQueryBuilder();
379
-				$query->update('filecache_extended')
380
-					->whereFileId($id)
381
-					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
382
-						return $query->expr()->orX(
383
-							$query->expr()->neq($key, $query->createNamedParameter($value)),
384
-							$query->expr()->isNull($key)
385
-						);
386
-					}, array_keys($extensionValues), array_values($extensionValues))));
387
-
388
-				foreach ($extensionValues as $key => $value) {
389
-					$query->set($key, $query->createNamedParameter($value));
390
-				}
391
-
392
-				$query->execute();
393
-			}
394
-		}
395
-
396
-		$path = $this->getPathById($id);
397
-		// path can still be null if the file doesn't exist
398
-		if ($path !== null) {
399
-			$this->eventDispatcher->dispatch(CacheUpdateEvent::class, new CacheUpdateEvent($this->storage, $path, $id));
400
-		}
401
-	}
402
-
403
-	/**
404
-	 * extract query parts and params array from data array
405
-	 *
406
-	 * @param array $data
407
-	 * @return array
408
-	 */
409
-	protected function normalizeData(array $data): array {
410
-		$fields = [
411
-			'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
412
-			'etag', 'permissions', 'checksum', 'storage'];
413
-		$extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
414
-
415
-		$doNotCopyStorageMTime = false;
416
-		if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
417
-			// this horrific magic tells it to not copy storage_mtime to mtime
418
-			unset($data['mtime']);
419
-			$doNotCopyStorageMTime = true;
420
-		}
421
-
422
-		$params = [];
423
-		$extensionParams = [];
424
-		foreach ($data as $name => $value) {
425
-			if (array_search($name, $fields) !== false) {
426
-				if ($name === 'path') {
427
-					$params['path_hash'] = md5($value);
428
-				} else if ($name === 'mimetype') {
429
-					$params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
430
-					$value = $this->mimetypeLoader->getId($value);
431
-				} else if ($name === 'storage_mtime') {
432
-					if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
433
-						$params['mtime'] = $value;
434
-					}
435
-				} else if ($name === 'encrypted') {
436
-					if (isset($data['encryptedVersion'])) {
437
-						$value = $data['encryptedVersion'];
438
-					} else {
439
-						// Boolean to integer conversion
440
-						$value = $value ? 1 : 0;
441
-					}
442
-				}
443
-				$params[$name] = $value;
444
-			}
445
-			if (array_search($name, $extensionFields) !== false) {
446
-				$extensionParams[$name] = $value;
447
-			}
448
-		}
449
-		return [$params, array_filter($extensionParams)];
450
-	}
451
-
452
-	/**
453
-	 * get the file id for a file
454
-	 *
455
-	 * A file id is a numeric id for a file or folder that's unique within an owncloud instance which stays the same for the lifetime of a file
456
-	 *
457
-	 * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
458
-	 *
459
-	 * @param string $file
460
-	 * @return int
461
-	 */
462
-	public function getId($file) {
463
-		// normalize file
464
-		$file = $this->normalize($file);
465
-
466
-		$query = $this->getQueryBuilder();
467
-		$query->select('fileid')
468
-			->from('filecache')
469
-			->whereStorageId()
470
-			->wherePath($file);
471
-
472
-		$id = $query->execute()->fetchColumn();
473
-		return $id === false ? -1 : (int)$id;
474
-	}
475
-
476
-	/**
477
-	 * get the id of the parent folder of a file
478
-	 *
479
-	 * @param string $file
480
-	 * @return int
481
-	 */
482
-	public function getParentId($file) {
483
-		if ($file === '') {
484
-			return -1;
485
-		} else {
486
-			$parent = $this->getParentPath($file);
487
-			return (int)$this->getId($parent);
488
-		}
489
-	}
490
-
491
-	private function getParentPath($path) {
492
-		$parent = dirname($path);
493
-		if ($parent === '.') {
494
-			$parent = '';
495
-		}
496
-		return $parent;
497
-	}
498
-
499
-	/**
500
-	 * check if a file is available in the cache
501
-	 *
502
-	 * @param string $file
503
-	 * @return bool
504
-	 */
505
-	public function inCache($file) {
506
-		return $this->getId($file) != -1;
507
-	}
508
-
509
-	/**
510
-	 * remove a file or folder from the cache
511
-	 *
512
-	 * when removing a folder from the cache all files and folders inside the folder will be removed as well
513
-	 *
514
-	 * @param string $file
515
-	 */
516
-	public function remove($file) {
517
-		$entry = $this->get($file);
518
-
519
-		if ($entry) {
520
-			$query = $this->getQueryBuilder();
521
-			$query->delete('filecache')
522
-				->whereFileId($entry->getId());
523
-			$query->execute();
524
-
525
-			$query = $this->getQueryBuilder();
526
-			$query->delete('filecache_extended')
527
-				->whereFileId($entry->getId());
528
-			$query->execute();
529
-
530
-			if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
531
-				$this->removeChildren($entry);
532
-			}
533
-		}
534
-	}
535
-
536
-	/**
537
-	 * Get all sub folders of a folder
538
-	 *
539
-	 * @param ICacheEntry $entry the cache entry of the folder to get the subfolders for
540
-	 * @return ICacheEntry[] the cache entries for the subfolders
541
-	 */
542
-	private function getSubFolders(ICacheEntry $entry) {
543
-		$children = $this->getFolderContentsById($entry->getId());
544
-		return array_filter($children, function ($child) {
545
-			return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
546
-		});
547
-	}
548
-
549
-	/**
550
-	 * Recursively remove all children of a folder
551
-	 *
552
-	 * @param ICacheEntry $entry the cache entry of the folder to remove the children of
553
-	 * @throws \OC\DatabaseException
554
-	 */
555
-	private function removeChildren(ICacheEntry $entry) {
556
-		$children = $this->getFolderContentsById($entry->getId());
557
-		$childIds = array_map(function(ICacheEntry $cacheEntry) {
558
-			return $cacheEntry->getId();
559
-		}, $children);
560
-		$childFolders = array_filter($children, function ($child) {
561
-			return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
562
-		});
563
-		foreach ($childFolders as $folder) {
564
-			$this->removeChildren($folder);
565
-		}
566
-
567
-		$query = $this->getQueryBuilder();
568
-		$query->delete('filecache')
569
-			->whereParent($entry->getId());
570
-		$query->execute();
571
-
572
-		$query = $this->getQueryBuilder();
573
-		$query->delete('filecache_extended')
574
-			->where($query->expr()->in('fileid', $query->createNamedParameter($childIds, IQueryBuilder::PARAM_INT_ARRAY)));
575
-		$query->execute();
576
-	}
577
-
578
-	/**
579
-	 * Move a file or folder in the cache
580
-	 *
581
-	 * @param string $source
582
-	 * @param string $target
583
-	 */
584
-	public function move($source, $target) {
585
-		$this->moveFromCache($this, $source, $target);
586
-	}
587
-
588
-	/**
589
-	 * Get the storage id and path needed for a move
590
-	 *
591
-	 * @param string $path
592
-	 * @return array [$storageId, $internalPath]
593
-	 */
594
-	protected function getMoveInfo($path) {
595
-		return [$this->getNumericStorageId(), $path];
596
-	}
597
-
598
-	/**
599
-	 * Move a file or folder in the cache
600
-	 *
601
-	 * @param \OCP\Files\Cache\ICache $sourceCache
602
-	 * @param string $sourcePath
603
-	 * @param string $targetPath
604
-	 * @throws \OC\DatabaseException
605
-	 * @throws \Exception if the given storages have an invalid id
606
-	 * @suppress SqlInjectionChecker
607
-	 */
608
-	public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
609
-		if ($sourceCache instanceof Cache) {
610
-			// normalize source and target
611
-			$sourcePath = $this->normalize($sourcePath);
612
-			$targetPath = $this->normalize($targetPath);
613
-
614
-			$sourceData = $sourceCache->get($sourcePath);
615
-			$sourceId = $sourceData['fileid'];
616
-			$newParentId = $this->getParentId($targetPath);
617
-
618
-			[$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
619
-			[$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
620
-
621
-			if (is_null($sourceStorageId) || $sourceStorageId === false) {
622
-				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
623
-			}
624
-			if (is_null($targetStorageId) || $targetStorageId === false) {
625
-				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
626
-			}
627
-
628
-			$this->connection->beginTransaction();
629
-			if ($sourceData['mimetype'] === 'httpd/unix-directory') {
630
-				//update all child entries
631
-				$sourceLength = mb_strlen($sourcePath);
632
-				$query = $this->connection->getQueryBuilder();
633
-
634
-				$fun = $query->func();
635
-				$newPathFunction = $fun->concat(
636
-					$query->createNamedParameter($targetPath),
637
-					$fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
638
-				);
639
-				$query->update('filecache')
640
-					->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
641
-					->set('path_hash', $fun->md5($newPathFunction))
642
-					->set('path', $newPathFunction)
643
-					->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
644
-					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
645
-
646
-				try {
647
-					$query->execute();
648
-				} catch (\OC\DatabaseException $e) {
649
-					$this->connection->rollBack();
650
-					throw $e;
651
-				}
652
-			}
653
-
654
-			$query = $this->getQueryBuilder();
655
-			$query->update('filecache')
656
-				->set('storage', $query->createNamedParameter($targetStorageId))
657
-				->set('path', $query->createNamedParameter($targetPath))
658
-				->set('path_hash', $query->createNamedParameter(md5($targetPath)))
659
-				->set('name', $query->createNamedParameter(basename($targetPath)))
660
-				->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
661
-				->whereFileId($sourceId);
662
-			$query->execute();
663
-
664
-			$this->connection->commit();
665
-		} else {
666
-			$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
667
-		}
668
-	}
669
-
670
-	/**
671
-	 * remove all entries for files that are stored on the storage from the cache
672
-	 */
673
-	public function clear() {
674
-		$query = $this->getQueryBuilder();
675
-		$query->delete('filecache')
676
-			->whereStorageId();
677
-		$query->execute();
678
-
679
-		$query = $this->connection->getQueryBuilder();
680
-		$query->delete('storages')
681
-			->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
682
-		$query->execute();
683
-	}
684
-
685
-	/**
686
-	 * Get the scan status of a file
687
-	 *
688
-	 * - Cache::NOT_FOUND: File is not in the cache
689
-	 * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
690
-	 * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
691
-	 * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
692
-	 *
693
-	 * @param string $file
694
-	 *
695
-	 * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
696
-	 */
697
-	public function getStatus($file) {
698
-		// normalize file
699
-		$file = $this->normalize($file);
700
-
701
-		$query = $this->getQueryBuilder();
702
-		$query->select('size')
703
-			->from('filecache')
704
-			->whereStorageId()
705
-			->wherePath($file);
706
-		$size = $query->execute()->fetchColumn();
707
-		if ($size !== false) {
708
-			if ((int)$size === -1) {
709
-				return self::SHALLOW;
710
-			} else {
711
-				return self::COMPLETE;
712
-			}
713
-		} else {
714
-			if (isset($this->partial[$file])) {
715
-				return self::PARTIAL;
716
-			} else {
717
-				return self::NOT_FOUND;
718
-			}
719
-		}
720
-	}
721
-
722
-	/**
723
-	 * search for files matching $pattern
724
-	 *
725
-	 * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
726
-	 * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
727
-	 */
728
-	public function search($pattern) {
729
-		// normalize pattern
730
-		$pattern = $this->normalize($pattern);
731
-
732
-		if ($pattern === '%%') {
733
-			return [];
734
-		}
735
-
736
-		$query = $this->getQueryBuilder();
737
-		$query->selectFileCache()
738
-			->whereStorageId()
739
-			->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
740
-
741
-		return array_map(function (array $data) {
742
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
743
-		}, $query->execute()->fetchAll());
744
-	}
745
-
746
-	/**
747
-	 * @param Statement $result
748
-	 * @return CacheEntry[]
749
-	 */
750
-	private function searchResultToCacheEntries(Statement $result) {
751
-		$files = $result->fetchAll();
752
-
753
-		return array_map(function (array $data) {
754
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
755
-		}, $files);
756
-	}
757
-
758
-	/**
759
-	 * search for files by mimetype
760
-	 *
761
-	 * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
762
-	 *        where it will search for all mimetypes in the group ('image/*')
763
-	 * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
764
-	 */
765
-	public function searchByMime($mimetype) {
766
-		$mimeId = $this->mimetypeLoader->getId($mimetype);
767
-
768
-		$query = $this->getQueryBuilder();
769
-		$query->selectFileCache()
770
-			->whereStorageId();
771
-
772
-		if (strpos($mimetype, '/')) {
773
-			$query->andWhere($query->expr()->eq('mimetype', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
774
-		} else {
775
-			$query->andWhere($query->expr()->eq('mimepart', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
776
-		}
777
-
778
-		return array_map(function (array $data) {
779
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
780
-		}, $query->execute()->fetchAll());
781
-	}
782
-
783
-	public function searchQuery(ISearchQuery $searchQuery) {
784
-		$builder = $this->getQueryBuilder();
785
-
786
-		$query = $builder->selectFileCache('file');
787
-
788
-		$query->whereStorageId();
789
-
790
-		if ($this->querySearchHelper->shouldJoinTags($searchQuery->getSearchOperation())) {
791
-			$query
792
-				->innerJoin('file', 'vcategory_to_object', 'tagmap', $builder->expr()->eq('file.fileid', 'tagmap.objid'))
793
-				->innerJoin('tagmap', 'vcategory', 'tag', $builder->expr()->andX(
794
-					$builder->expr()->eq('tagmap.type', 'tag.type'),
795
-					$builder->expr()->eq('tagmap.categoryid', 'tag.id')
796
-				))
797
-				->andWhere($builder->expr()->eq('tag.type', $builder->createNamedParameter('files')))
798
-				->andWhere($builder->expr()->eq('tag.uid', $builder->createNamedParameter($searchQuery->getUser()->getUID())));
799
-		}
800
-
801
-		$searchExpr = $this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation());
802
-		if ($searchExpr) {
803
-			$query->andWhere($searchExpr);
804
-		}
805
-
806
-		if ($searchQuery->limitToHome() && ($this instanceof HomeCache)) {
807
-			$query->andWhere($builder->expr()->like('path', $query->expr()->literal('files/%')));
808
-		}
809
-
810
-		$this->querySearchHelper->addSearchOrdersToQuery($query, $searchQuery->getOrder());
811
-
812
-		if ($searchQuery->getLimit()) {
813
-			$query->setMaxResults($searchQuery->getLimit());
814
-		}
815
-		if ($searchQuery->getOffset()) {
816
-			$query->setFirstResult($searchQuery->getOffset());
817
-		}
818
-
819
-		$result = $query->execute();
820
-		return $this->searchResultToCacheEntries($result);
821
-	}
822
-
823
-	/**
824
-	 * Re-calculate the folder size and the size of all parent folders
825
-	 *
826
-	 * @param string|boolean $path
827
-	 * @param array $data (optional) meta data of the folder
828
-	 */
829
-	public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
830
-		$this->calculateFolderSize($path, $data);
831
-		if ($path !== '') {
832
-			$parent = dirname($path);
833
-			if ($parent === '.' or $parent === '/') {
834
-				$parent = '';
835
-			}
836
-			if ($isBackgroundScan) {
837
-				$parentData = $this->get($parent);
838
-				if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
839
-					$this->correctFolderSize($parent, $parentData, $isBackgroundScan);
840
-				}
841
-			} else {
842
-				$this->correctFolderSize($parent);
843
-			}
844
-		}
845
-	}
846
-
847
-	/**
848
-	 * get the incomplete count that shares parent $folder
849
-	 *
850
-	 * @param int $fileId the file id of the folder
851
-	 * @return int
852
-	 */
853
-	public function getIncompleteChildrenCount($fileId) {
854
-		if ($fileId > -1) {
855
-			$query = $this->getQueryBuilder();
856
-			$query->select($query->func()->count())
857
-				->from('filecache')
858
-				->whereParent($fileId)
859
-				->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
860
-
861
-			return (int)$query->execute()->fetchColumn();
862
-		}
863
-		return -1;
864
-	}
865
-
866
-	/**
867
-	 * calculate the size of a folder and set it in the cache
868
-	 *
869
-	 * @param string $path
870
-	 * @param array $entry (optional) meta data of the folder
871
-	 * @return int
872
-	 */
873
-	public function calculateFolderSize($path, $entry = null) {
874
-		$totalSize = 0;
875
-		if (is_null($entry) or !isset($entry['fileid'])) {
876
-			$entry = $this->get($path);
877
-		}
878
-		if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
879
-			$id = $entry['fileid'];
880
-
881
-			$query = $this->getQueryBuilder();
882
-			$query->selectAlias($query->func()->sum('size'), 'f1')
883
-				->selectAlias($query->func()->min('size'), 'f2')
884
-				->from('filecache')
885
-				->whereStorageId()
886
-				->whereParent($id);
887
-
888
-			if ($row = $query->execute()->fetch()) {
889
-				[$sum, $min] = array_values($row);
890
-				$sum = 0 + $sum;
891
-				$min = 0 + $min;
892
-				if ($min === -1) {
893
-					$totalSize = $min;
894
-				} else {
895
-					$totalSize = $sum;
896
-				}
897
-				if ($entry['size'] !== $totalSize) {
898
-					$this->update($id, ['size' => $totalSize]);
899
-				}
900
-			}
901
-		}
902
-		return $totalSize;
903
-	}
904
-
905
-	/**
906
-	 * get all file ids on the files on the storage
907
-	 *
908
-	 * @return int[]
909
-	 */
910
-	public function getAll() {
911
-		$query = $this->getQueryBuilder();
912
-		$query->select('fileid')
913
-			->from('filecache')
914
-			->whereStorageId();
915
-
916
-		return array_map(function ($id) {
917
-			return (int)$id;
918
-		}, $query->execute()->fetchAll(\PDO::FETCH_COLUMN));
919
-	}
920
-
921
-	/**
922
-	 * find a folder in the cache which has not been fully scanned
923
-	 *
924
-	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
925
-	 * use the one with the highest id gives the best result with the background scanner, since that is most
926
-	 * likely the folder where we stopped scanning previously
927
-	 *
928
-	 * @return string|bool the path of the folder or false when no folder matched
929
-	 */
930
-	public function getIncomplete() {
931
-		$query = $this->getQueryBuilder();
932
-		$query->select('path')
933
-			->from('filecache')
934
-			->whereStorageId()
935
-			->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
936
-			->orderBy('fileid', 'DESC');
937
-
938
-		return $query->execute()->fetchColumn();
939
-	}
940
-
941
-	/**
942
-	 * get the path of a file on this storage by it's file id
943
-	 *
944
-	 * @param int $id the file id of the file or folder to search
945
-	 * @return string|null the path of the file (relative to the storage) or null if a file with the given id does not exists within this cache
946
-	 */
947
-	public function getPathById($id) {
948
-		$query = $this->getQueryBuilder();
949
-		$query->select('path')
950
-			->from('filecache')
951
-			->whereStorageId()
952
-			->whereFileId($id);
953
-
954
-		$path = $query->execute()->fetchColumn();
955
-		return $path === false ? null : $path;
956
-	}
957
-
958
-	/**
959
-	 * get the storage id of the storage for a file and the internal path of the file
960
-	 * unlike getPathById this does not limit the search to files on this storage and
961
-	 * instead does a global search in the cache table
962
-	 *
963
-	 * @param int $id
964
-	 * @return array first element holding the storage id, second the path
965
-	 * @deprecated use getPathById() instead
966
-	 */
967
-	static public function getById($id) {
968
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
969
-		$query->select('path', 'storage')
970
-			->from('filecache')
971
-			->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
972
-		if ($row = $query->execute()->fetch()) {
973
-			$numericId = $row['storage'];
974
-			$path = $row['path'];
975
-		} else {
976
-			return null;
977
-		}
978
-
979
-		if ($id = Storage::getStorageId($numericId)) {
980
-			return [$id, $path];
981
-		} else {
982
-			return null;
983
-		}
984
-	}
985
-
986
-	/**
987
-	 * normalize the given path
988
-	 *
989
-	 * @param string $path
990
-	 * @return string
991
-	 */
992
-	public function normalize($path) {
993
-
994
-		return trim(\OC_Util::normalizeUnicode($path), '/');
995
-	}
64
+    use MoveFromCacheTrait {
65
+        MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
66
+    }
67
+
68
+    /**
69
+     * @var array partial data for the cache
70
+     */
71
+    protected $partial = [];
72
+
73
+    /**
74
+     * @var string
75
+     */
76
+    protected $storageId;
77
+
78
+    private $storage;
79
+
80
+    /**
81
+     * @var Storage $storageCache
82
+     */
83
+    protected $storageCache;
84
+
85
+    /** @var IMimeTypeLoader */
86
+    protected $mimetypeLoader;
87
+
88
+    /**
89
+     * @var IDBConnection
90
+     */
91
+    protected $connection;
92
+
93
+    protected $eventDispatcher;
94
+
95
+    /** @var QuerySearchHelper */
96
+    protected $querySearchHelper;
97
+
98
+    /**
99
+     * @param IStorage $storage
100
+     */
101
+    public function __construct(IStorage $storage) {
102
+        $this->storageId = $storage->getId();
103
+        $this->storage = $storage;
104
+        if (strlen($this->storageId) > 64) {
105
+            $this->storageId = md5($this->storageId);
106
+        }
107
+
108
+        $this->storageCache = new Storage($storage);
109
+        $this->mimetypeLoader = \OC::$server->getMimeTypeLoader();
110
+        $this->connection = \OC::$server->getDatabaseConnection();
111
+        $this->eventDispatcher = \OC::$server->getEventDispatcher();
112
+        $this->querySearchHelper = new QuerySearchHelper($this->mimetypeLoader);
113
+    }
114
+
115
+    private function getQueryBuilder() {
116
+        return new CacheQueryBuilder(
117
+            $this->connection,
118
+            \OC::$server->getSystemConfig(),
119
+            \OC::$server->getLogger(),
120
+            $this
121
+        );
122
+    }
123
+
124
+    /**
125
+     * Get the numeric storage id for this cache's storage
126
+     *
127
+     * @return int
128
+     */
129
+    public function getNumericStorageId() {
130
+        return $this->storageCache->getNumericId();
131
+    }
132
+
133
+    /**
134
+     * get the stored metadata of a file or folder
135
+     *
136
+     * @param string | int $file either the path of a file or folder or the file id for a file or folder
137
+     * @return ICacheEntry|false the cache entry as array of false if the file is not found in the cache
138
+     */
139
+    public function get($file) {
140
+        $query = $this->getQueryBuilder();
141
+        $query->selectFileCache();
142
+
143
+        if (is_string($file) or $file == '') {
144
+            // normalize file
145
+            $file = $this->normalize($file);
146
+
147
+            $query->whereStorageId()
148
+                ->wherePath($file);
149
+        } else { //file id
150
+            $query->whereFileId($file);
151
+        }
152
+
153
+        $data = $query->execute()->fetch();
154
+
155
+        //merge partial data
156
+        if (!$data and is_string($file) and isset($this->partial[$file])) {
157
+            return $this->partial[$file];
158
+        } else if (!$data) {
159
+            return $data;
160
+        } else {
161
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
162
+        }
163
+    }
164
+
165
+    /**
166
+     * Create a CacheEntry from database row
167
+     *
168
+     * @param array $data
169
+     * @param IMimeTypeLoader $mimetypeLoader
170
+     * @return CacheEntry
171
+     */
172
+    public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
173
+        //fix types
174
+        $data['fileid'] = (int)$data['fileid'];
175
+        $data['parent'] = (int)$data['parent'];
176
+        $data['size'] = 0 + $data['size'];
177
+        $data['mtime'] = (int)$data['mtime'];
178
+        $data['storage_mtime'] = (int)$data['storage_mtime'];
179
+        $data['encryptedVersion'] = (int)$data['encrypted'];
180
+        $data['encrypted'] = (bool)$data['encrypted'];
181
+        $data['storage_id'] = $data['storage'];
182
+        $data['storage'] = (int)$data['storage'];
183
+        $data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
184
+        $data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
185
+        if ($data['storage_mtime'] == 0) {
186
+            $data['storage_mtime'] = $data['mtime'];
187
+        }
188
+        $data['permissions'] = (int)$data['permissions'];
189
+        if (isset($data['creation_time'])) {
190
+            $data['creation_time'] = (int) $data['creation_time'];
191
+        }
192
+        if (isset($data['upload_time'])) {
193
+            $data['upload_time'] = (int) $data['upload_time'];
194
+        }
195
+        return new CacheEntry($data);
196
+    }
197
+
198
+    /**
199
+     * get the metadata of all files stored in $folder
200
+     *
201
+     * @param string $folder
202
+     * @return ICacheEntry[]
203
+     */
204
+    public function getFolderContents($folder) {
205
+        $fileId = $this->getId($folder);
206
+        return $this->getFolderContentsById($fileId);
207
+    }
208
+
209
+    /**
210
+     * get the metadata of all files stored in $folder
211
+     *
212
+     * @param int $fileId the file id of the folder
213
+     * @return ICacheEntry[]
214
+     */
215
+    public function getFolderContentsById($fileId) {
216
+        if ($fileId > -1) {
217
+            $query = $this->getQueryBuilder();
218
+            $query->selectFileCache()
219
+                ->whereParent($fileId)
220
+                ->orderBy('name', 'ASC');
221
+
222
+            $files = $query->execute()->fetchAll();
223
+            return array_map(function (array $data) {
224
+                return self::cacheEntryFromData($data, $this->mimetypeLoader);
225
+            }, $files);
226
+        }
227
+        return [];
228
+    }
229
+
230
+    /**
231
+     * insert or update meta data for a file or folder
232
+     *
233
+     * @param string $file
234
+     * @param array $data
235
+     *
236
+     * @return int file id
237
+     * @throws \RuntimeException
238
+     */
239
+    public function put($file, array $data) {
240
+        if (($id = $this->getId($file)) > -1) {
241
+            $this->update($id, $data);
242
+            return $id;
243
+        } else {
244
+            return $this->insert($file, $data);
245
+        }
246
+    }
247
+
248
+    /**
249
+     * insert meta data for a new file or folder
250
+     *
251
+     * @param string $file
252
+     * @param array $data
253
+     *
254
+     * @return int file id
255
+     * @throws \RuntimeException
256
+     *
257
+     * @suppress SqlInjectionChecker
258
+     */
259
+    public function insert($file, array $data) {
260
+        // normalize file
261
+        $file = $this->normalize($file);
262
+
263
+        if (isset($this->partial[$file])) { //add any saved partial data
264
+            $data = array_merge($this->partial[$file], $data);
265
+            unset($this->partial[$file]);
266
+        }
267
+
268
+        $requiredFields = ['size', 'mtime', 'mimetype'];
269
+        foreach ($requiredFields as $field) {
270
+            if (!isset($data[$field])) { //data not complete save as partial and return
271
+                $this->partial[$file] = $data;
272
+                return -1;
273
+            }
274
+        }
275
+
276
+        $data['path'] = $file;
277
+        if (!isset($data['parent'])) {
278
+            $data['parent'] = $this->getParentId($file);
279
+        }
280
+        $data['name'] = basename($file);
281
+
282
+        [$values, $extensionValues] = $this->normalizeData($data);
283
+        $values['storage'] = $this->getNumericStorageId();
284
+
285
+        try {
286
+            $builder = $this->connection->getQueryBuilder();
287
+            $builder->insert('filecache');
288
+
289
+            foreach ($values as $column => $value) {
290
+                $builder->setValue($column, $builder->createNamedParameter($value));
291
+            }
292
+
293
+            if ($builder->execute()) {
294
+                $fileId = $builder->getLastInsertId();
295
+
296
+                if (count($extensionValues)) {
297
+                    $query = $this->getQueryBuilder();
298
+                    $query->insert('filecache_extended');
299
+
300
+                    $query->setValue('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT));
301
+                    foreach ($extensionValues as $column => $value) {
302
+                        $query->setValue($column, $query->createNamedParameter($value));
303
+                    }
304
+                    $query->execute();
305
+                }
306
+
307
+                $this->eventDispatcher->dispatch(CacheInsertEvent::class, new CacheInsertEvent($this->storage, $file, $fileId));
308
+                return $fileId;
309
+            }
310
+        } catch (UniqueConstraintViolationException $e) {
311
+            // entry exists already
312
+            if ($this->connection->inTransaction()) {
313
+                $this->connection->commit();
314
+                $this->connection->beginTransaction();
315
+            }
316
+        }
317
+
318
+        // The file was created in the mean time
319
+        if (($id = $this->getId($file)) > -1) {
320
+            $this->update($id, $data);
321
+            return $id;
322
+        } else {
323
+            throw new \RuntimeException('File entry could not be inserted but could also not be selected with getId() in order to perform an update. Please try again.');
324
+        }
325
+    }
326
+
327
+    /**
328
+     * update the metadata of an existing file or folder in the cache
329
+     *
330
+     * @param int $id the fileid of the existing file or folder
331
+     * @param array $data [$key => $value] the metadata to update, only the fields provided in the array will be updated, non-provided values will remain unchanged
332
+     */
333
+    public function update($id, array $data) {
334
+
335
+        if (isset($data['path'])) {
336
+            // normalize path
337
+            $data['path'] = $this->normalize($data['path']);
338
+        }
339
+
340
+        if (isset($data['name'])) {
341
+            // normalize path
342
+            $data['name'] = $this->normalize($data['name']);
343
+        }
344
+
345
+        [$values, $extensionValues] = $this->normalizeData($data);
346
+
347
+        if (count($values)) {
348
+            $query = $this->getQueryBuilder();
349
+
350
+            $query->update('filecache')
351
+                ->whereFileId($id)
352
+                ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
353
+                    return $query->expr()->orX(
354
+                        $query->expr()->neq($key, $query->createNamedParameter($value)),
355
+                        $query->expr()->isNull($key)
356
+                    );
357
+                }, array_keys($values), array_values($values))));
358
+
359
+            foreach ($values as $key => $value) {
360
+                $query->set($key, $query->createNamedParameter($value));
361
+            }
362
+
363
+            $query->execute();
364
+        }
365
+
366
+        if (count($extensionValues)) {
367
+            try {
368
+                $query = $this->getQueryBuilder();
369
+                $query->insert('filecache_extended');
370
+
371
+                $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
372
+                foreach ($extensionValues as $column => $value) {
373
+                    $query->setValue($column, $query->createNamedParameter($value));
374
+                }
375
+
376
+                $query->execute();
377
+            } catch (UniqueConstraintViolationException $e) {
378
+                $query = $this->getQueryBuilder();
379
+                $query->update('filecache_extended')
380
+                    ->whereFileId($id)
381
+                    ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
382
+                        return $query->expr()->orX(
383
+                            $query->expr()->neq($key, $query->createNamedParameter($value)),
384
+                            $query->expr()->isNull($key)
385
+                        );
386
+                    }, array_keys($extensionValues), array_values($extensionValues))));
387
+
388
+                foreach ($extensionValues as $key => $value) {
389
+                    $query->set($key, $query->createNamedParameter($value));
390
+                }
391
+
392
+                $query->execute();
393
+            }
394
+        }
395
+
396
+        $path = $this->getPathById($id);
397
+        // path can still be null if the file doesn't exist
398
+        if ($path !== null) {
399
+            $this->eventDispatcher->dispatch(CacheUpdateEvent::class, new CacheUpdateEvent($this->storage, $path, $id));
400
+        }
401
+    }
402
+
403
+    /**
404
+     * extract query parts and params array from data array
405
+     *
406
+     * @param array $data
407
+     * @return array
408
+     */
409
+    protected function normalizeData(array $data): array {
410
+        $fields = [
411
+            'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
412
+            'etag', 'permissions', 'checksum', 'storage'];
413
+        $extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
414
+
415
+        $doNotCopyStorageMTime = false;
416
+        if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
417
+            // this horrific magic tells it to not copy storage_mtime to mtime
418
+            unset($data['mtime']);
419
+            $doNotCopyStorageMTime = true;
420
+        }
421
+
422
+        $params = [];
423
+        $extensionParams = [];
424
+        foreach ($data as $name => $value) {
425
+            if (array_search($name, $fields) !== false) {
426
+                if ($name === 'path') {
427
+                    $params['path_hash'] = md5($value);
428
+                } else if ($name === 'mimetype') {
429
+                    $params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
430
+                    $value = $this->mimetypeLoader->getId($value);
431
+                } else if ($name === 'storage_mtime') {
432
+                    if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
433
+                        $params['mtime'] = $value;
434
+                    }
435
+                } else if ($name === 'encrypted') {
436
+                    if (isset($data['encryptedVersion'])) {
437
+                        $value = $data['encryptedVersion'];
438
+                    } else {
439
+                        // Boolean to integer conversion
440
+                        $value = $value ? 1 : 0;
441
+                    }
442
+                }
443
+                $params[$name] = $value;
444
+            }
445
+            if (array_search($name, $extensionFields) !== false) {
446
+                $extensionParams[$name] = $value;
447
+            }
448
+        }
449
+        return [$params, array_filter($extensionParams)];
450
+    }
451
+
452
+    /**
453
+     * get the file id for a file
454
+     *
455
+     * A file id is a numeric id for a file or folder that's unique within an owncloud instance which stays the same for the lifetime of a file
456
+     *
457
+     * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
458
+     *
459
+     * @param string $file
460
+     * @return int
461
+     */
462
+    public function getId($file) {
463
+        // normalize file
464
+        $file = $this->normalize($file);
465
+
466
+        $query = $this->getQueryBuilder();
467
+        $query->select('fileid')
468
+            ->from('filecache')
469
+            ->whereStorageId()
470
+            ->wherePath($file);
471
+
472
+        $id = $query->execute()->fetchColumn();
473
+        return $id === false ? -1 : (int)$id;
474
+    }
475
+
476
+    /**
477
+     * get the id of the parent folder of a file
478
+     *
479
+     * @param string $file
480
+     * @return int
481
+     */
482
+    public function getParentId($file) {
483
+        if ($file === '') {
484
+            return -1;
485
+        } else {
486
+            $parent = $this->getParentPath($file);
487
+            return (int)$this->getId($parent);
488
+        }
489
+    }
490
+
491
+    private function getParentPath($path) {
492
+        $parent = dirname($path);
493
+        if ($parent === '.') {
494
+            $parent = '';
495
+        }
496
+        return $parent;
497
+    }
498
+
499
+    /**
500
+     * check if a file is available in the cache
501
+     *
502
+     * @param string $file
503
+     * @return bool
504
+     */
505
+    public function inCache($file) {
506
+        return $this->getId($file) != -1;
507
+    }
508
+
509
+    /**
510
+     * remove a file or folder from the cache
511
+     *
512
+     * when removing a folder from the cache all files and folders inside the folder will be removed as well
513
+     *
514
+     * @param string $file
515
+     */
516
+    public function remove($file) {
517
+        $entry = $this->get($file);
518
+
519
+        if ($entry) {
520
+            $query = $this->getQueryBuilder();
521
+            $query->delete('filecache')
522
+                ->whereFileId($entry->getId());
523
+            $query->execute();
524
+
525
+            $query = $this->getQueryBuilder();
526
+            $query->delete('filecache_extended')
527
+                ->whereFileId($entry->getId());
528
+            $query->execute();
529
+
530
+            if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
531
+                $this->removeChildren($entry);
532
+            }
533
+        }
534
+    }
535
+
536
+    /**
537
+     * Get all sub folders of a folder
538
+     *
539
+     * @param ICacheEntry $entry the cache entry of the folder to get the subfolders for
540
+     * @return ICacheEntry[] the cache entries for the subfolders
541
+     */
542
+    private function getSubFolders(ICacheEntry $entry) {
543
+        $children = $this->getFolderContentsById($entry->getId());
544
+        return array_filter($children, function ($child) {
545
+            return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
546
+        });
547
+    }
548
+
549
+    /**
550
+     * Recursively remove all children of a folder
551
+     *
552
+     * @param ICacheEntry $entry the cache entry of the folder to remove the children of
553
+     * @throws \OC\DatabaseException
554
+     */
555
+    private function removeChildren(ICacheEntry $entry) {
556
+        $children = $this->getFolderContentsById($entry->getId());
557
+        $childIds = array_map(function(ICacheEntry $cacheEntry) {
558
+            return $cacheEntry->getId();
559
+        }, $children);
560
+        $childFolders = array_filter($children, function ($child) {
561
+            return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
562
+        });
563
+        foreach ($childFolders as $folder) {
564
+            $this->removeChildren($folder);
565
+        }
566
+
567
+        $query = $this->getQueryBuilder();
568
+        $query->delete('filecache')
569
+            ->whereParent($entry->getId());
570
+        $query->execute();
571
+
572
+        $query = $this->getQueryBuilder();
573
+        $query->delete('filecache_extended')
574
+            ->where($query->expr()->in('fileid', $query->createNamedParameter($childIds, IQueryBuilder::PARAM_INT_ARRAY)));
575
+        $query->execute();
576
+    }
577
+
578
+    /**
579
+     * Move a file or folder in the cache
580
+     *
581
+     * @param string $source
582
+     * @param string $target
583
+     */
584
+    public function move($source, $target) {
585
+        $this->moveFromCache($this, $source, $target);
586
+    }
587
+
588
+    /**
589
+     * Get the storage id and path needed for a move
590
+     *
591
+     * @param string $path
592
+     * @return array [$storageId, $internalPath]
593
+     */
594
+    protected function getMoveInfo($path) {
595
+        return [$this->getNumericStorageId(), $path];
596
+    }
597
+
598
+    /**
599
+     * Move a file or folder in the cache
600
+     *
601
+     * @param \OCP\Files\Cache\ICache $sourceCache
602
+     * @param string $sourcePath
603
+     * @param string $targetPath
604
+     * @throws \OC\DatabaseException
605
+     * @throws \Exception if the given storages have an invalid id
606
+     * @suppress SqlInjectionChecker
607
+     */
608
+    public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
609
+        if ($sourceCache instanceof Cache) {
610
+            // normalize source and target
611
+            $sourcePath = $this->normalize($sourcePath);
612
+            $targetPath = $this->normalize($targetPath);
613
+
614
+            $sourceData = $sourceCache->get($sourcePath);
615
+            $sourceId = $sourceData['fileid'];
616
+            $newParentId = $this->getParentId($targetPath);
617
+
618
+            [$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
619
+            [$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
620
+
621
+            if (is_null($sourceStorageId) || $sourceStorageId === false) {
622
+                throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
623
+            }
624
+            if (is_null($targetStorageId) || $targetStorageId === false) {
625
+                throw new \Exception('Invalid target storage id: ' . $targetStorageId);
626
+            }
627
+
628
+            $this->connection->beginTransaction();
629
+            if ($sourceData['mimetype'] === 'httpd/unix-directory') {
630
+                //update all child entries
631
+                $sourceLength = mb_strlen($sourcePath);
632
+                $query = $this->connection->getQueryBuilder();
633
+
634
+                $fun = $query->func();
635
+                $newPathFunction = $fun->concat(
636
+                    $query->createNamedParameter($targetPath),
637
+                    $fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
638
+                );
639
+                $query->update('filecache')
640
+                    ->set('storage', $query->createNamedParameter($targetStorageId, IQueryBuilder::PARAM_INT))
641
+                    ->set('path_hash', $fun->md5($newPathFunction))
642
+                    ->set('path', $newPathFunction)
643
+                    ->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
644
+                    ->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
645
+
646
+                try {
647
+                    $query->execute();
648
+                } catch (\OC\DatabaseException $e) {
649
+                    $this->connection->rollBack();
650
+                    throw $e;
651
+                }
652
+            }
653
+
654
+            $query = $this->getQueryBuilder();
655
+            $query->update('filecache')
656
+                ->set('storage', $query->createNamedParameter($targetStorageId))
657
+                ->set('path', $query->createNamedParameter($targetPath))
658
+                ->set('path_hash', $query->createNamedParameter(md5($targetPath)))
659
+                ->set('name', $query->createNamedParameter(basename($targetPath)))
660
+                ->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
661
+                ->whereFileId($sourceId);
662
+            $query->execute();
663
+
664
+            $this->connection->commit();
665
+        } else {
666
+            $this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
667
+        }
668
+    }
669
+
670
+    /**
671
+     * remove all entries for files that are stored on the storage from the cache
672
+     */
673
+    public function clear() {
674
+        $query = $this->getQueryBuilder();
675
+        $query->delete('filecache')
676
+            ->whereStorageId();
677
+        $query->execute();
678
+
679
+        $query = $this->connection->getQueryBuilder();
680
+        $query->delete('storages')
681
+            ->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
682
+        $query->execute();
683
+    }
684
+
685
+    /**
686
+     * Get the scan status of a file
687
+     *
688
+     * - Cache::NOT_FOUND: File is not in the cache
689
+     * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
690
+     * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
691
+     * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
692
+     *
693
+     * @param string $file
694
+     *
695
+     * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
696
+     */
697
+    public function getStatus($file) {
698
+        // normalize file
699
+        $file = $this->normalize($file);
700
+
701
+        $query = $this->getQueryBuilder();
702
+        $query->select('size')
703
+            ->from('filecache')
704
+            ->whereStorageId()
705
+            ->wherePath($file);
706
+        $size = $query->execute()->fetchColumn();
707
+        if ($size !== false) {
708
+            if ((int)$size === -1) {
709
+                return self::SHALLOW;
710
+            } else {
711
+                return self::COMPLETE;
712
+            }
713
+        } else {
714
+            if (isset($this->partial[$file])) {
715
+                return self::PARTIAL;
716
+            } else {
717
+                return self::NOT_FOUND;
718
+            }
719
+        }
720
+    }
721
+
722
+    /**
723
+     * search for files matching $pattern
724
+     *
725
+     * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
726
+     * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
727
+     */
728
+    public function search($pattern) {
729
+        // normalize pattern
730
+        $pattern = $this->normalize($pattern);
731
+
732
+        if ($pattern === '%%') {
733
+            return [];
734
+        }
735
+
736
+        $query = $this->getQueryBuilder();
737
+        $query->selectFileCache()
738
+            ->whereStorageId()
739
+            ->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
740
+
741
+        return array_map(function (array $data) {
742
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
743
+        }, $query->execute()->fetchAll());
744
+    }
745
+
746
+    /**
747
+     * @param Statement $result
748
+     * @return CacheEntry[]
749
+     */
750
+    private function searchResultToCacheEntries(Statement $result) {
751
+        $files = $result->fetchAll();
752
+
753
+        return array_map(function (array $data) {
754
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
755
+        }, $files);
756
+    }
757
+
758
+    /**
759
+     * search for files by mimetype
760
+     *
761
+     * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
762
+     *        where it will search for all mimetypes in the group ('image/*')
763
+     * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
764
+     */
765
+    public function searchByMime($mimetype) {
766
+        $mimeId = $this->mimetypeLoader->getId($mimetype);
767
+
768
+        $query = $this->getQueryBuilder();
769
+        $query->selectFileCache()
770
+            ->whereStorageId();
771
+
772
+        if (strpos($mimetype, '/')) {
773
+            $query->andWhere($query->expr()->eq('mimetype', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
774
+        } else {
775
+            $query->andWhere($query->expr()->eq('mimepart', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
776
+        }
777
+
778
+        return array_map(function (array $data) {
779
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
780
+        }, $query->execute()->fetchAll());
781
+    }
782
+
783
+    public function searchQuery(ISearchQuery $searchQuery) {
784
+        $builder = $this->getQueryBuilder();
785
+
786
+        $query = $builder->selectFileCache('file');
787
+
788
+        $query->whereStorageId();
789
+
790
+        if ($this->querySearchHelper->shouldJoinTags($searchQuery->getSearchOperation())) {
791
+            $query
792
+                ->innerJoin('file', 'vcategory_to_object', 'tagmap', $builder->expr()->eq('file.fileid', 'tagmap.objid'))
793
+                ->innerJoin('tagmap', 'vcategory', 'tag', $builder->expr()->andX(
794
+                    $builder->expr()->eq('tagmap.type', 'tag.type'),
795
+                    $builder->expr()->eq('tagmap.categoryid', 'tag.id')
796
+                ))
797
+                ->andWhere($builder->expr()->eq('tag.type', $builder->createNamedParameter('files')))
798
+                ->andWhere($builder->expr()->eq('tag.uid', $builder->createNamedParameter($searchQuery->getUser()->getUID())));
799
+        }
800
+
801
+        $searchExpr = $this->querySearchHelper->searchOperatorToDBExpr($builder, $searchQuery->getSearchOperation());
802
+        if ($searchExpr) {
803
+            $query->andWhere($searchExpr);
804
+        }
805
+
806
+        if ($searchQuery->limitToHome() && ($this instanceof HomeCache)) {
807
+            $query->andWhere($builder->expr()->like('path', $query->expr()->literal('files/%')));
808
+        }
809
+
810
+        $this->querySearchHelper->addSearchOrdersToQuery($query, $searchQuery->getOrder());
811
+
812
+        if ($searchQuery->getLimit()) {
813
+            $query->setMaxResults($searchQuery->getLimit());
814
+        }
815
+        if ($searchQuery->getOffset()) {
816
+            $query->setFirstResult($searchQuery->getOffset());
817
+        }
818
+
819
+        $result = $query->execute();
820
+        return $this->searchResultToCacheEntries($result);
821
+    }
822
+
823
+    /**
824
+     * Re-calculate the folder size and the size of all parent folders
825
+     *
826
+     * @param string|boolean $path
827
+     * @param array $data (optional) meta data of the folder
828
+     */
829
+    public function correctFolderSize($path, $data = null, $isBackgroundScan = false) {
830
+        $this->calculateFolderSize($path, $data);
831
+        if ($path !== '') {
832
+            $parent = dirname($path);
833
+            if ($parent === '.' or $parent === '/') {
834
+                $parent = '';
835
+            }
836
+            if ($isBackgroundScan) {
837
+                $parentData = $this->get($parent);
838
+                if ($parentData['size'] !== -1 && $this->getIncompleteChildrenCount($parentData['fileid']) === 0) {
839
+                    $this->correctFolderSize($parent, $parentData, $isBackgroundScan);
840
+                }
841
+            } else {
842
+                $this->correctFolderSize($parent);
843
+            }
844
+        }
845
+    }
846
+
847
+    /**
848
+     * get the incomplete count that shares parent $folder
849
+     *
850
+     * @param int $fileId the file id of the folder
851
+     * @return int
852
+     */
853
+    public function getIncompleteChildrenCount($fileId) {
854
+        if ($fileId > -1) {
855
+            $query = $this->getQueryBuilder();
856
+            $query->select($query->func()->count())
857
+                ->from('filecache')
858
+                ->whereParent($fileId)
859
+                ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
860
+
861
+            return (int)$query->execute()->fetchColumn();
862
+        }
863
+        return -1;
864
+    }
865
+
866
+    /**
867
+     * calculate the size of a folder and set it in the cache
868
+     *
869
+     * @param string $path
870
+     * @param array $entry (optional) meta data of the folder
871
+     * @return int
872
+     */
873
+    public function calculateFolderSize($path, $entry = null) {
874
+        $totalSize = 0;
875
+        if (is_null($entry) or !isset($entry['fileid'])) {
876
+            $entry = $this->get($path);
877
+        }
878
+        if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
879
+            $id = $entry['fileid'];
880
+
881
+            $query = $this->getQueryBuilder();
882
+            $query->selectAlias($query->func()->sum('size'), 'f1')
883
+                ->selectAlias($query->func()->min('size'), 'f2')
884
+                ->from('filecache')
885
+                ->whereStorageId()
886
+                ->whereParent($id);
887
+
888
+            if ($row = $query->execute()->fetch()) {
889
+                [$sum, $min] = array_values($row);
890
+                $sum = 0 + $sum;
891
+                $min = 0 + $min;
892
+                if ($min === -1) {
893
+                    $totalSize = $min;
894
+                } else {
895
+                    $totalSize = $sum;
896
+                }
897
+                if ($entry['size'] !== $totalSize) {
898
+                    $this->update($id, ['size' => $totalSize]);
899
+                }
900
+            }
901
+        }
902
+        return $totalSize;
903
+    }
904
+
905
+    /**
906
+     * get all file ids on the files on the storage
907
+     *
908
+     * @return int[]
909
+     */
910
+    public function getAll() {
911
+        $query = $this->getQueryBuilder();
912
+        $query->select('fileid')
913
+            ->from('filecache')
914
+            ->whereStorageId();
915
+
916
+        return array_map(function ($id) {
917
+            return (int)$id;
918
+        }, $query->execute()->fetchAll(\PDO::FETCH_COLUMN));
919
+    }
920
+
921
+    /**
922
+     * find a folder in the cache which has not been fully scanned
923
+     *
924
+     * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
925
+     * use the one with the highest id gives the best result with the background scanner, since that is most
926
+     * likely the folder where we stopped scanning previously
927
+     *
928
+     * @return string|bool the path of the folder or false when no folder matched
929
+     */
930
+    public function getIncomplete() {
931
+        $query = $this->getQueryBuilder();
932
+        $query->select('path')
933
+            ->from('filecache')
934
+            ->whereStorageId()
935
+            ->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
936
+            ->orderBy('fileid', 'DESC');
937
+
938
+        return $query->execute()->fetchColumn();
939
+    }
940
+
941
+    /**
942
+     * get the path of a file on this storage by it's file id
943
+     *
944
+     * @param int $id the file id of the file or folder to search
945
+     * @return string|null the path of the file (relative to the storage) or null if a file with the given id does not exists within this cache
946
+     */
947
+    public function getPathById($id) {
948
+        $query = $this->getQueryBuilder();
949
+        $query->select('path')
950
+            ->from('filecache')
951
+            ->whereStorageId()
952
+            ->whereFileId($id);
953
+
954
+        $path = $query->execute()->fetchColumn();
955
+        return $path === false ? null : $path;
956
+    }
957
+
958
+    /**
959
+     * get the storage id of the storage for a file and the internal path of the file
960
+     * unlike getPathById this does not limit the search to files on this storage and
961
+     * instead does a global search in the cache table
962
+     *
963
+     * @param int $id
964
+     * @return array first element holding the storage id, second the path
965
+     * @deprecated use getPathById() instead
966
+     */
967
+    static public function getById($id) {
968
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
969
+        $query->select('path', 'storage')
970
+            ->from('filecache')
971
+            ->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
972
+        if ($row = $query->execute()->fetch()) {
973
+            $numericId = $row['storage'];
974
+            $path = $row['path'];
975
+        } else {
976
+            return null;
977
+        }
978
+
979
+        if ($id = Storage::getStorageId($numericId)) {
980
+            return [$id, $path];
981
+        } else {
982
+            return null;
983
+        }
984
+    }
985
+
986
+    /**
987
+     * normalize the given path
988
+     *
989
+     * @param string $path
990
+     * @return string
991
+     */
992
+    public function normalize($path) {
993
+
994
+        return trim(\OC_Util::normalizeUnicode($path), '/');
995
+    }
996 996
 }
Please login to merge, or discard this patch.
Spacing   +25 added lines, -25 removed lines patch added patch discarded remove patch
@@ -171,21 +171,21 @@  discard block
 block discarded – undo
171 171
 	 */
172 172
 	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
173 173
 		//fix types
174
-		$data['fileid'] = (int)$data['fileid'];
175
-		$data['parent'] = (int)$data['parent'];
174
+		$data['fileid'] = (int) $data['fileid'];
175
+		$data['parent'] = (int) $data['parent'];
176 176
 		$data['size'] = 0 + $data['size'];
177
-		$data['mtime'] = (int)$data['mtime'];
178
-		$data['storage_mtime'] = (int)$data['storage_mtime'];
179
-		$data['encryptedVersion'] = (int)$data['encrypted'];
180
-		$data['encrypted'] = (bool)$data['encrypted'];
177
+		$data['mtime'] = (int) $data['mtime'];
178
+		$data['storage_mtime'] = (int) $data['storage_mtime'];
179
+		$data['encryptedVersion'] = (int) $data['encrypted'];
180
+		$data['encrypted'] = (bool) $data['encrypted'];
181 181
 		$data['storage_id'] = $data['storage'];
182
-		$data['storage'] = (int)$data['storage'];
182
+		$data['storage'] = (int) $data['storage'];
183 183
 		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
184 184
 		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
185 185
 		if ($data['storage_mtime'] == 0) {
186 186
 			$data['storage_mtime'] = $data['mtime'];
187 187
 		}
188
-		$data['permissions'] = (int)$data['permissions'];
188
+		$data['permissions'] = (int) $data['permissions'];
189 189
 		if (isset($data['creation_time'])) {
190 190
 			$data['creation_time'] = (int) $data['creation_time'];
191 191
 		}
@@ -220,7 +220,7 @@  discard block
 block discarded – undo
220 220
 				->orderBy('name', 'ASC');
221 221
 
222 222
 			$files = $query->execute()->fetchAll();
223
-			return array_map(function (array $data) {
223
+			return array_map(function(array $data) {
224 224
 				return self::cacheEntryFromData($data, $this->mimetypeLoader);
225 225
 			}, $files);
226 226
 		}
@@ -349,7 +349,7 @@  discard block
 block discarded – undo
349 349
 
350 350
 			$query->update('filecache')
351 351
 				->whereFileId($id)
352
-				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
352
+				->andWhere($query->expr()->orX(...array_map(function($key, $value) use ($query) {
353 353
 					return $query->expr()->orX(
354 354
 						$query->expr()->neq($key, $query->createNamedParameter($value)),
355 355
 						$query->expr()->isNull($key)
@@ -378,7 +378,7 @@  discard block
 block discarded – undo
378 378
 				$query = $this->getQueryBuilder();
379 379
 				$query->update('filecache_extended')
380 380
 					->whereFileId($id)
381
-					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
381
+					->andWhere($query->expr()->orX(...array_map(function($key, $value) use ($query) {
382 382
 						return $query->expr()->orX(
383 383
 							$query->expr()->neq($key, $query->createNamedParameter($value)),
384 384
 							$query->expr()->isNull($key)
@@ -470,7 +470,7 @@  discard block
 block discarded – undo
470 470
 			->wherePath($file);
471 471
 
472 472
 		$id = $query->execute()->fetchColumn();
473
-		return $id === false ? -1 : (int)$id;
473
+		return $id === false ? -1 : (int) $id;
474 474
 	}
475 475
 
476 476
 	/**
@@ -484,7 +484,7 @@  discard block
 block discarded – undo
484 484
 			return -1;
485 485
 		} else {
486 486
 			$parent = $this->getParentPath($file);
487
-			return (int)$this->getId($parent);
487
+			return (int) $this->getId($parent);
488 488
 		}
489 489
 	}
490 490
 
@@ -541,7 +541,7 @@  discard block
 block discarded – undo
541 541
 	 */
542 542
 	private function getSubFolders(ICacheEntry $entry) {
543 543
 		$children = $this->getFolderContentsById($entry->getId());
544
-		return array_filter($children, function ($child) {
544
+		return array_filter($children, function($child) {
545 545
 			return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
546 546
 		});
547 547
 	}
@@ -557,7 +557,7 @@  discard block
 block discarded – undo
557 557
 		$childIds = array_map(function(ICacheEntry $cacheEntry) {
558 558
 			return $cacheEntry->getId();
559 559
 		}, $children);
560
-		$childFolders = array_filter($children, function ($child) {
560
+		$childFolders = array_filter($children, function($child) {
561 561
 			return $child->getMimeType() == FileInfo::MIMETYPE_FOLDER;
562 562
 		});
563 563
 		foreach ($childFolders as $folder) {
@@ -619,10 +619,10 @@  discard block
 block discarded – undo
619 619
 			[$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
620 620
 
621 621
 			if (is_null($sourceStorageId) || $sourceStorageId === false) {
622
-				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
622
+				throw new \Exception('Invalid source storage id: '.$sourceStorageId);
623 623
 			}
624 624
 			if (is_null($targetStorageId) || $targetStorageId === false) {
625
-				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
625
+				throw new \Exception('Invalid target storage id: '.$targetStorageId);
626 626
 			}
627 627
 
628 628
 			$this->connection->beginTransaction();
@@ -641,7 +641,7 @@  discard block
 block discarded – undo
641 641
 					->set('path_hash', $fun->md5($newPathFunction))
642 642
 					->set('path', $newPathFunction)
643 643
 					->where($query->expr()->eq('storage', $query->createNamedParameter($sourceStorageId, IQueryBuilder::PARAM_INT)))
644
-					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath) . '/%')));
644
+					->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($sourcePath).'/%')));
645 645
 
646 646
 				try {
647 647
 					$query->execute();
@@ -705,7 +705,7 @@  discard block
 block discarded – undo
705 705
 			->wherePath($file);
706 706
 		$size = $query->execute()->fetchColumn();
707 707
 		if ($size !== false) {
708
-			if ((int)$size === -1) {
708
+			if ((int) $size === -1) {
709 709
 				return self::SHALLOW;
710 710
 			} else {
711 711
 				return self::COMPLETE;
@@ -738,7 +738,7 @@  discard block
 block discarded – undo
738 738
 			->whereStorageId()
739 739
 			->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern)));
740 740
 
741
-		return array_map(function (array $data) {
741
+		return array_map(function(array $data) {
742 742
 			return self::cacheEntryFromData($data, $this->mimetypeLoader);
743 743
 		}, $query->execute()->fetchAll());
744 744
 	}
@@ -750,7 +750,7 @@  discard block
 block discarded – undo
750 750
 	private function searchResultToCacheEntries(Statement $result) {
751 751
 		$files = $result->fetchAll();
752 752
 
753
-		return array_map(function (array $data) {
753
+		return array_map(function(array $data) {
754 754
 			return self::cacheEntryFromData($data, $this->mimetypeLoader);
755 755
 		}, $files);
756 756
 	}
@@ -775,7 +775,7 @@  discard block
 block discarded – undo
775 775
 			$query->andWhere($query->expr()->eq('mimepart', $query->createNamedParameter($mimeId, IQueryBuilder::PARAM_INT)));
776 776
 		}
777 777
 
778
-		return array_map(function (array $data) {
778
+		return array_map(function(array $data) {
779 779
 			return self::cacheEntryFromData($data, $this->mimetypeLoader);
780 780
 		}, $query->execute()->fetchAll());
781 781
 	}
@@ -858,7 +858,7 @@  discard block
 block discarded – undo
858 858
 				->whereParent($fileId)
859 859
 				->andWhere($query->expr()->lt('size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
860 860
 
861
-			return (int)$query->execute()->fetchColumn();
861
+			return (int) $query->execute()->fetchColumn();
862 862
 		}
863 863
 		return -1;
864 864
 	}
@@ -913,8 +913,8 @@  discard block
 block discarded – undo
913 913
 			->from('filecache')
914 914
 			->whereStorageId();
915 915
 
916
-		return array_map(function ($id) {
917
-			return (int)$id;
916
+		return array_map(function($id) {
917
+			return (int) $id;
918 918
 		}, $query->execute()->fetchAll(\PDO::FETCH_COLUMN));
919 919
 	}
920 920
 
Please login to merge, or discard this patch.
lib/private/Files/Storage/Local.php 1 patch
Indentation   +502 added lines, -502 removed lines patch added patch discarded remove patch
@@ -52,506 +52,506 @@
 block discarded – undo
52 52
  * for local filestore, we only have to map the paths
53 53
  */
54 54
 class Local extends \OC\Files\Storage\Common {
55
-	protected $datadir;
56
-
57
-	protected $dataDirLength;
58
-
59
-	protected $allowSymlinks = false;
60
-
61
-	protected $realDataDir;
62
-
63
-	public function __construct($arguments) {
64
-		if (!isset($arguments['datadir']) || !is_string($arguments['datadir'])) {
65
-			throw new \InvalidArgumentException('No data directory set for local storage');
66
-		}
67
-		$this->datadir = str_replace('//', '/', $arguments['datadir']);
68
-		// some crazy code uses a local storage on root...
69
-		if ($this->datadir === '/') {
70
-			$this->realDataDir = $this->datadir;
71
-		} else {
72
-			$realPath = realpath($this->datadir) ?: $this->datadir;
73
-			$this->realDataDir = rtrim($realPath, '/') . '/';
74
-		}
75
-		if (substr($this->datadir, -1) !== '/') {
76
-			$this->datadir .= '/';
77
-		}
78
-		$this->dataDirLength = strlen($this->realDataDir);
79
-	}
80
-
81
-	public function __destruct() {
82
-	}
83
-
84
-	public function getId() {
85
-		return 'local::' . $this->datadir;
86
-	}
87
-
88
-	public function mkdir($path) {
89
-		return @mkdir($this->getSourcePath($path), 0777, true);
90
-	}
91
-
92
-	public function rmdir($path) {
93
-		if (!$this->isDeletable($path)) {
94
-			return false;
95
-		}
96
-		try {
97
-			$it = new \RecursiveIteratorIterator(
98
-				new \RecursiveDirectoryIterator($this->getSourcePath($path)),
99
-				\RecursiveIteratorIterator::CHILD_FIRST
100
-			);
101
-			/**
102
-			 * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
103
-			 * This bug is fixed in PHP 5.5.9 or before
104
-			 * See #8376
105
-			 */
106
-			$it->rewind();
107
-			while ($it->valid()) {
108
-				/**
109
-				 * @var \SplFileInfo $file
110
-				 */
111
-				$file = $it->current();
112
-				if (in_array($file->getBasename(), ['.', '..'])) {
113
-					$it->next();
114
-					continue;
115
-				} else if ($file->isDir()) {
116
-					rmdir($file->getPathname());
117
-				} else if ($file->isFile() || $file->isLink()) {
118
-					unlink($file->getPathname());
119
-				}
120
-				$it->next();
121
-			}
122
-			return rmdir($this->getSourcePath($path));
123
-		} catch (\UnexpectedValueException $e) {
124
-			return false;
125
-		}
126
-	}
127
-
128
-	public function opendir($path) {
129
-		return opendir($this->getSourcePath($path));
130
-	}
131
-
132
-	public function is_dir($path) {
133
-		if (substr($path, -1) == '/') {
134
-			$path = substr($path, 0, -1);
135
-		}
136
-		return is_dir($this->getSourcePath($path));
137
-	}
138
-
139
-	public function is_file($path) {
140
-		return is_file($this->getSourcePath($path));
141
-	}
142
-
143
-	public function stat($path) {
144
-		clearstatcache();
145
-		$fullPath = $this->getSourcePath($path);
146
-		$statResult = stat($fullPath);
147
-		if (PHP_INT_SIZE === 4 && !$this->is_dir($path)) {
148
-			$filesize = $this->filesize($path);
149
-			$statResult['size'] = $filesize;
150
-			$statResult[7] = $filesize;
151
-		}
152
-		return $statResult;
153
-	}
154
-
155
-	/**
156
-	 * @inheritdoc
157
-	 */
158
-	public function getMetaData($path) {
159
-		$fullPath = $this->getSourcePath($path);
160
-		$stat = @stat($fullPath);
161
-		if (!$stat) {
162
-			return null;
163
-		}
164
-
165
-		$permissions = Constants::PERMISSION_SHARE;
166
-		$statPermissions = $stat['mode'];
167
-		$isDir = ($statPermissions & 0x4000) === 0x4000;
168
-		if ($statPermissions & 0x0100) {
169
-			$permissions += Constants::PERMISSION_READ;
170
-		}
171
-		if ($statPermissions & 0x0080) {
172
-			$permissions += Constants::PERMISSION_UPDATE;
173
-			if ($isDir) {
174
-				$permissions += Constants::PERMISSION_CREATE;
175
-			}
176
-		}
177
-
178
-		if (!($path === '' || $path === '/')) { // deletable depends on the parents unix permissions
179
-			$parent = dirname($fullPath);
180
-			if (is_writable($parent)) {
181
-				$permissions += Constants::PERMISSION_DELETE;
182
-			}
183
-		}
184
-
185
-		$data = [];
186
-		$data['mimetype'] = $isDir ? 'httpd/unix-directory' : \OC::$server->getMimeTypeDetector()->detectPath($path);
187
-		$data['mtime'] = $stat['mtime'];
188
-		if ($data['mtime'] === false) {
189
-			$data['mtime'] = time();
190
-		}
191
-		if ($isDir) {
192
-			$data['size'] = -1; //unknown
193
-		} else {
194
-			$data['size'] = $stat['size'];
195
-		}
196
-		$data['etag'] = $this->calculateEtag($path, $stat);
197
-		$data['storage_mtime'] = $data['mtime'];
198
-		$data['permissions'] = $permissions;
199
-
200
-		return $data;
201
-	}
202
-
203
-	public function filetype($path) {
204
-		$filetype = filetype($this->getSourcePath($path));
205
-		if ($filetype == 'link') {
206
-			$filetype = filetype(realpath($this->getSourcePath($path)));
207
-		}
208
-		return $filetype;
209
-	}
210
-
211
-	public function filesize($path) {
212
-		if ($this->is_dir($path)) {
213
-			return 0;
214
-		}
215
-		$fullPath = $this->getSourcePath($path);
216
-		if (PHP_INT_SIZE === 4) {
217
-			$helper = new \OC\LargeFileHelper;
218
-			return $helper->getFileSize($fullPath);
219
-		}
220
-		return filesize($fullPath);
221
-	}
222
-
223
-	public function isReadable($path) {
224
-		return is_readable($this->getSourcePath($path));
225
-	}
226
-
227
-	public function isUpdatable($path) {
228
-		return is_writable($this->getSourcePath($path));
229
-	}
230
-
231
-	public function file_exists($path) {
232
-		return file_exists($this->getSourcePath($path));
233
-	}
234
-
235
-	public function filemtime($path) {
236
-		$fullPath = $this->getSourcePath($path);
237
-		clearstatcache(true, $fullPath);
238
-		if (!$this->file_exists($path)) {
239
-			return false;
240
-		}
241
-		if (PHP_INT_SIZE === 4) {
242
-			$helper = new \OC\LargeFileHelper();
243
-			return $helper->getFileMtime($fullPath);
244
-		}
245
-		return filemtime($fullPath);
246
-	}
247
-
248
-	public function touch($path, $mtime = null) {
249
-		// sets the modification time of the file to the given value.
250
-		// If mtime is nil the current time is set.
251
-		// note that the access time of the file always changes to the current time.
252
-		if ($this->file_exists($path) and !$this->isUpdatable($path)) {
253
-			return false;
254
-		}
255
-		if (!is_null($mtime)) {
256
-			$result = @touch($this->getSourcePath($path), $mtime);
257
-		} else {
258
-			$result = @touch($this->getSourcePath($path));
259
-		}
260
-		if ($result) {
261
-			clearstatcache(true, $this->getSourcePath($path));
262
-		}
263
-
264
-		return $result;
265
-	}
266
-
267
-	public function file_get_contents($path) {
268
-		return file_get_contents($this->getSourcePath($path));
269
-	}
270
-
271
-	public function file_put_contents($path, $data) {
272
-		return file_put_contents($this->getSourcePath($path), $data);
273
-	}
274
-
275
-	public function unlink($path) {
276
-		if ($this->is_dir($path)) {
277
-			return $this->rmdir($path);
278
-		} else if ($this->is_file($path)) {
279
-			return unlink($this->getSourcePath($path));
280
-		} else {
281
-			return false;
282
-		}
283
-
284
-	}
285
-
286
-	private function treeContainsBlacklistedFile(string $path): bool {
287
-		$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));
288
-		foreach ($iterator as $file) {
289
-			/** @var \SplFileInfo $file */
290
-			if (Filesystem::isFileBlacklisted($file->getBasename())) {
291
-				return true;
292
-			}
293
-		}
294
-
295
-		return false;
296
-	}
297
-
298
-	public function rename($path1, $path2) {
299
-		$srcParent = dirname($path1);
300
-		$dstParent = dirname($path2);
301
-
302
-		if (!$this->isUpdatable($srcParent)) {
303
-			\OCP\Util::writeLog('core', 'unable to rename, source directory is not writable : ' . $srcParent, ILogger::ERROR);
304
-			return false;
305
-		}
306
-
307
-		if (!$this->isUpdatable($dstParent)) {
308
-			\OCP\Util::writeLog('core', 'unable to rename, destination directory is not writable : ' . $dstParent, ILogger::ERROR);
309
-			return false;
310
-		}
311
-
312
-		if (!$this->file_exists($path1)) {
313
-			\OCP\Util::writeLog('core', 'unable to rename, file does not exists : ' . $path1, ILogger::ERROR);
314
-			return false;
315
-		}
316
-
317
-		if ($this->is_dir($path2)) {
318
-			$this->rmdir($path2);
319
-		} else if ($this->is_file($path2)) {
320
-			$this->unlink($path2);
321
-		}
322
-
323
-		if ($this->is_dir($path1)) {
324
-			// we can't move folders across devices, use copy instead
325
-			$stat1 = stat(dirname($this->getSourcePath($path1)));
326
-			$stat2 = stat(dirname($this->getSourcePath($path2)));
327
-			if ($stat1['dev'] !== $stat2['dev']) {
328
-				$result = $this->copy($path1, $path2);
329
-				if ($result) {
330
-					$result &= $this->rmdir($path1);
331
-				}
332
-				return $result;
333
-			}
334
-
335
-			if ($this->treeContainsBlacklistedFile($this->getSourcePath($path1))) {
336
-				throw new ForbiddenException('Invalid path', false);
337
-			}
338
-		}
339
-
340
-		return rename($this->getSourcePath($path1), $this->getSourcePath($path2));
341
-	}
342
-
343
-	public function copy($path1, $path2) {
344
-		if ($this->is_dir($path1)) {
345
-			return parent::copy($path1, $path2);
346
-		} else {
347
-			return copy($this->getSourcePath($path1), $this->getSourcePath($path2));
348
-		}
349
-	}
350
-
351
-	public function fopen($path, $mode) {
352
-		return fopen($this->getSourcePath($path), $mode);
353
-	}
354
-
355
-	public function hash($type, $path, $raw = false) {
356
-		return hash_file($type, $this->getSourcePath($path), $raw);
357
-	}
358
-
359
-	public function free_space($path) {
360
-		$sourcePath = $this->getSourcePath($path);
361
-		// using !is_dir because $sourcePath might be a part file or
362
-		// non-existing file, so we'd still want to use the parent dir
363
-		// in such cases
364
-		if (!is_dir($sourcePath)) {
365
-			// disk_free_space doesn't work on files
366
-			$sourcePath = dirname($sourcePath);
367
-		}
368
-		$space = @disk_free_space($sourcePath);
369
-		if ($space === false || is_null($space)) {
370
-			return \OCP\Files\FileInfo::SPACE_UNKNOWN;
371
-		}
372
-		return $space;
373
-	}
374
-
375
-	public function search($query) {
376
-		return $this->searchInDir($query);
377
-	}
378
-
379
-	public function getLocalFile($path) {
380
-		return $this->getSourcePath($path);
381
-	}
382
-
383
-	public function getLocalFolder($path) {
384
-		return $this->getSourcePath($path);
385
-	}
386
-
387
-	/**
388
-	 * @param string $query
389
-	 * @param string $dir
390
-	 * @return array
391
-	 */
392
-	protected function searchInDir($query, $dir = '') {
393
-		$files = [];
394
-		$physicalDir = $this->getSourcePath($dir);
395
-		foreach (scandir($physicalDir) as $item) {
396
-			if (\OC\Files\Filesystem::isIgnoredDir($item))
397
-				continue;
398
-			$physicalItem = $physicalDir . '/' . $item;
399
-
400
-			if (strstr(strtolower($item), strtolower($query)) !== false) {
401
-				$files[] = $dir . '/' . $item;
402
-			}
403
-			if (is_dir($physicalItem)) {
404
-				$files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
405
-			}
406
-		}
407
-		return $files;
408
-	}
409
-
410
-	/**
411
-	 * check if a file or folder has been updated since $time
412
-	 *
413
-	 * @param string $path
414
-	 * @param int $time
415
-	 * @return bool
416
-	 */
417
-	public function hasUpdated($path, $time) {
418
-		if ($this->file_exists($path)) {
419
-			return $this->filemtime($path) > $time;
420
-		} else {
421
-			return true;
422
-		}
423
-	}
424
-
425
-	/**
426
-	 * Get the source path (on disk) of a given path
427
-	 *
428
-	 * @param string $path
429
-	 * @return string
430
-	 * @throws ForbiddenException
431
-	 */
432
-	public function getSourcePath($path) {
433
-		if (Filesystem::isFileBlacklisted($path)) {
434
-			throw new ForbiddenException('Invalid path', false);
435
-		}
436
-
437
-		$fullPath = $this->datadir . $path;
438
-		$currentPath = $path;
439
-		if ($this->allowSymlinks || $currentPath === '') {
440
-			return $fullPath;
441
-		}
442
-		$pathToResolve = $fullPath;
443
-		$realPath = realpath($pathToResolve);
444
-		while ($realPath === false) { // for non existing files check the parent directory
445
-			$currentPath = dirname($currentPath);
446
-			if ($currentPath === '' || $currentPath === '.') {
447
-				return $fullPath;
448
-			}
449
-			$realPath = realpath($this->datadir . $currentPath);
450
-		}
451
-		if ($realPath) {
452
-			$realPath = $realPath . '/';
453
-		}
454
-		if (substr($realPath, 0, $this->dataDirLength) === $this->realDataDir) {
455
-			return $fullPath;
456
-		}
457
-
458
-		\OCP\Util::writeLog('core', "Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", ILogger::ERROR);
459
-		throw new ForbiddenException('Following symlinks is not allowed', false);
460
-	}
461
-
462
-	/**
463
-	 * {@inheritdoc}
464
-	 */
465
-	public function isLocal() {
466
-		return true;
467
-	}
468
-
469
-	/**
470
-	 * get the ETag for a file or folder
471
-	 *
472
-	 * @param string $path
473
-	 * @return string
474
-	 */
475
-	public function getETag($path) {
476
-		return $this->calculateEtag($path, $this->stat($path));
477
-	}
478
-
479
-	private function calculateEtag(string $path, array $stat): string {
480
-		if ($stat['mode'] & 0x4000) { // is_dir
481
-			return parent::getETag($path);
482
-		} else {
483
-			if ($stat === false) {
484
-				return md5('');
485
-			}
486
-
487
-			$toHash = '';
488
-			if (isset($stat['mtime'])) {
489
-				$toHash .= $stat['mtime'];
490
-			}
491
-			if (isset($stat['ino'])) {
492
-				$toHash .= $stat['ino'];
493
-			}
494
-			if (isset($stat['dev'])) {
495
-				$toHash .= $stat['dev'];
496
-			}
497
-			if (isset($stat['size'])) {
498
-				$toHash .= $stat['size'];
499
-			}
500
-
501
-			return md5($toHash);
502
-		}
503
-	}
504
-
505
-	/**
506
-	 * @param IStorage $sourceStorage
507
-	 * @param string $sourceInternalPath
508
-	 * @param string $targetInternalPath
509
-	 * @param bool $preserveMtime
510
-	 * @return bool
511
-	 */
512
-	public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
513
-		if ($sourceStorage->instanceOfStorage(Local::class)) {
514
-			if ($sourceStorage->instanceOfStorage(Jail::class)) {
515
-				/**
516
-				 * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
517
-				 */
518
-				$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
519
-			}
520
-			/**
521
-			 * @var \OC\Files\Storage\Local $sourceStorage
522
-			 */
523
-			$rootStorage = new Local(['datadir' => '/']);
524
-			return $rootStorage->copy($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
525
-		} else {
526
-			return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
527
-		}
528
-	}
529
-
530
-	/**
531
-	 * @param IStorage $sourceStorage
532
-	 * @param string $sourceInternalPath
533
-	 * @param string $targetInternalPath
534
-	 * @return bool
535
-	 */
536
-	public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
537
-		if ($sourceStorage->instanceOfStorage(Local::class)) {
538
-			if ($sourceStorage->instanceOfStorage(Jail::class)) {
539
-				/**
540
-				 * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
541
-				 */
542
-				$sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
543
-			}
544
-			/**
545
-			 * @var \OC\Files\Storage\Local $sourceStorage
546
-			 */
547
-			$rootStorage = new Local(['datadir' => '/']);
548
-			return $rootStorage->rename($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
549
-		} else {
550
-			return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
551
-		}
552
-	}
553
-
554
-	public function writeStream(string $path, $stream, int $size = null): int {
555
-		return (int)file_put_contents($this->getSourcePath($path), $stream);
556
-	}
55
+    protected $datadir;
56
+
57
+    protected $dataDirLength;
58
+
59
+    protected $allowSymlinks = false;
60
+
61
+    protected $realDataDir;
62
+
63
+    public function __construct($arguments) {
64
+        if (!isset($arguments['datadir']) || !is_string($arguments['datadir'])) {
65
+            throw new \InvalidArgumentException('No data directory set for local storage');
66
+        }
67
+        $this->datadir = str_replace('//', '/', $arguments['datadir']);
68
+        // some crazy code uses a local storage on root...
69
+        if ($this->datadir === '/') {
70
+            $this->realDataDir = $this->datadir;
71
+        } else {
72
+            $realPath = realpath($this->datadir) ?: $this->datadir;
73
+            $this->realDataDir = rtrim($realPath, '/') . '/';
74
+        }
75
+        if (substr($this->datadir, -1) !== '/') {
76
+            $this->datadir .= '/';
77
+        }
78
+        $this->dataDirLength = strlen($this->realDataDir);
79
+    }
80
+
81
+    public function __destruct() {
82
+    }
83
+
84
+    public function getId() {
85
+        return 'local::' . $this->datadir;
86
+    }
87
+
88
+    public function mkdir($path) {
89
+        return @mkdir($this->getSourcePath($path), 0777, true);
90
+    }
91
+
92
+    public function rmdir($path) {
93
+        if (!$this->isDeletable($path)) {
94
+            return false;
95
+        }
96
+        try {
97
+            $it = new \RecursiveIteratorIterator(
98
+                new \RecursiveDirectoryIterator($this->getSourcePath($path)),
99
+                \RecursiveIteratorIterator::CHILD_FIRST
100
+            );
101
+            /**
102
+             * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
103
+             * This bug is fixed in PHP 5.5.9 or before
104
+             * See #8376
105
+             */
106
+            $it->rewind();
107
+            while ($it->valid()) {
108
+                /**
109
+                 * @var \SplFileInfo $file
110
+                 */
111
+                $file = $it->current();
112
+                if (in_array($file->getBasename(), ['.', '..'])) {
113
+                    $it->next();
114
+                    continue;
115
+                } else if ($file->isDir()) {
116
+                    rmdir($file->getPathname());
117
+                } else if ($file->isFile() || $file->isLink()) {
118
+                    unlink($file->getPathname());
119
+                }
120
+                $it->next();
121
+            }
122
+            return rmdir($this->getSourcePath($path));
123
+        } catch (\UnexpectedValueException $e) {
124
+            return false;
125
+        }
126
+    }
127
+
128
+    public function opendir($path) {
129
+        return opendir($this->getSourcePath($path));
130
+    }
131
+
132
+    public function is_dir($path) {
133
+        if (substr($path, -1) == '/') {
134
+            $path = substr($path, 0, -1);
135
+        }
136
+        return is_dir($this->getSourcePath($path));
137
+    }
138
+
139
+    public function is_file($path) {
140
+        return is_file($this->getSourcePath($path));
141
+    }
142
+
143
+    public function stat($path) {
144
+        clearstatcache();
145
+        $fullPath = $this->getSourcePath($path);
146
+        $statResult = stat($fullPath);
147
+        if (PHP_INT_SIZE === 4 && !$this->is_dir($path)) {
148
+            $filesize = $this->filesize($path);
149
+            $statResult['size'] = $filesize;
150
+            $statResult[7] = $filesize;
151
+        }
152
+        return $statResult;
153
+    }
154
+
155
+    /**
156
+     * @inheritdoc
157
+     */
158
+    public function getMetaData($path) {
159
+        $fullPath = $this->getSourcePath($path);
160
+        $stat = @stat($fullPath);
161
+        if (!$stat) {
162
+            return null;
163
+        }
164
+
165
+        $permissions = Constants::PERMISSION_SHARE;
166
+        $statPermissions = $stat['mode'];
167
+        $isDir = ($statPermissions & 0x4000) === 0x4000;
168
+        if ($statPermissions & 0x0100) {
169
+            $permissions += Constants::PERMISSION_READ;
170
+        }
171
+        if ($statPermissions & 0x0080) {
172
+            $permissions += Constants::PERMISSION_UPDATE;
173
+            if ($isDir) {
174
+                $permissions += Constants::PERMISSION_CREATE;
175
+            }
176
+        }
177
+
178
+        if (!($path === '' || $path === '/')) { // deletable depends on the parents unix permissions
179
+            $parent = dirname($fullPath);
180
+            if (is_writable($parent)) {
181
+                $permissions += Constants::PERMISSION_DELETE;
182
+            }
183
+        }
184
+
185
+        $data = [];
186
+        $data['mimetype'] = $isDir ? 'httpd/unix-directory' : \OC::$server->getMimeTypeDetector()->detectPath($path);
187
+        $data['mtime'] = $stat['mtime'];
188
+        if ($data['mtime'] === false) {
189
+            $data['mtime'] = time();
190
+        }
191
+        if ($isDir) {
192
+            $data['size'] = -1; //unknown
193
+        } else {
194
+            $data['size'] = $stat['size'];
195
+        }
196
+        $data['etag'] = $this->calculateEtag($path, $stat);
197
+        $data['storage_mtime'] = $data['mtime'];
198
+        $data['permissions'] = $permissions;
199
+
200
+        return $data;
201
+    }
202
+
203
+    public function filetype($path) {
204
+        $filetype = filetype($this->getSourcePath($path));
205
+        if ($filetype == 'link') {
206
+            $filetype = filetype(realpath($this->getSourcePath($path)));
207
+        }
208
+        return $filetype;
209
+    }
210
+
211
+    public function filesize($path) {
212
+        if ($this->is_dir($path)) {
213
+            return 0;
214
+        }
215
+        $fullPath = $this->getSourcePath($path);
216
+        if (PHP_INT_SIZE === 4) {
217
+            $helper = new \OC\LargeFileHelper;
218
+            return $helper->getFileSize($fullPath);
219
+        }
220
+        return filesize($fullPath);
221
+    }
222
+
223
+    public function isReadable($path) {
224
+        return is_readable($this->getSourcePath($path));
225
+    }
226
+
227
+    public function isUpdatable($path) {
228
+        return is_writable($this->getSourcePath($path));
229
+    }
230
+
231
+    public function file_exists($path) {
232
+        return file_exists($this->getSourcePath($path));
233
+    }
234
+
235
+    public function filemtime($path) {
236
+        $fullPath = $this->getSourcePath($path);
237
+        clearstatcache(true, $fullPath);
238
+        if (!$this->file_exists($path)) {
239
+            return false;
240
+        }
241
+        if (PHP_INT_SIZE === 4) {
242
+            $helper = new \OC\LargeFileHelper();
243
+            return $helper->getFileMtime($fullPath);
244
+        }
245
+        return filemtime($fullPath);
246
+    }
247
+
248
+    public function touch($path, $mtime = null) {
249
+        // sets the modification time of the file to the given value.
250
+        // If mtime is nil the current time is set.
251
+        // note that the access time of the file always changes to the current time.
252
+        if ($this->file_exists($path) and !$this->isUpdatable($path)) {
253
+            return false;
254
+        }
255
+        if (!is_null($mtime)) {
256
+            $result = @touch($this->getSourcePath($path), $mtime);
257
+        } else {
258
+            $result = @touch($this->getSourcePath($path));
259
+        }
260
+        if ($result) {
261
+            clearstatcache(true, $this->getSourcePath($path));
262
+        }
263
+
264
+        return $result;
265
+    }
266
+
267
+    public function file_get_contents($path) {
268
+        return file_get_contents($this->getSourcePath($path));
269
+    }
270
+
271
+    public function file_put_contents($path, $data) {
272
+        return file_put_contents($this->getSourcePath($path), $data);
273
+    }
274
+
275
+    public function unlink($path) {
276
+        if ($this->is_dir($path)) {
277
+            return $this->rmdir($path);
278
+        } else if ($this->is_file($path)) {
279
+            return unlink($this->getSourcePath($path));
280
+        } else {
281
+            return false;
282
+        }
283
+
284
+    }
285
+
286
+    private function treeContainsBlacklistedFile(string $path): bool {
287
+        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path));
288
+        foreach ($iterator as $file) {
289
+            /** @var \SplFileInfo $file */
290
+            if (Filesystem::isFileBlacklisted($file->getBasename())) {
291
+                return true;
292
+            }
293
+        }
294
+
295
+        return false;
296
+    }
297
+
298
+    public function rename($path1, $path2) {
299
+        $srcParent = dirname($path1);
300
+        $dstParent = dirname($path2);
301
+
302
+        if (!$this->isUpdatable($srcParent)) {
303
+            \OCP\Util::writeLog('core', 'unable to rename, source directory is not writable : ' . $srcParent, ILogger::ERROR);
304
+            return false;
305
+        }
306
+
307
+        if (!$this->isUpdatable($dstParent)) {
308
+            \OCP\Util::writeLog('core', 'unable to rename, destination directory is not writable : ' . $dstParent, ILogger::ERROR);
309
+            return false;
310
+        }
311
+
312
+        if (!$this->file_exists($path1)) {
313
+            \OCP\Util::writeLog('core', 'unable to rename, file does not exists : ' . $path1, ILogger::ERROR);
314
+            return false;
315
+        }
316
+
317
+        if ($this->is_dir($path2)) {
318
+            $this->rmdir($path2);
319
+        } else if ($this->is_file($path2)) {
320
+            $this->unlink($path2);
321
+        }
322
+
323
+        if ($this->is_dir($path1)) {
324
+            // we can't move folders across devices, use copy instead
325
+            $stat1 = stat(dirname($this->getSourcePath($path1)));
326
+            $stat2 = stat(dirname($this->getSourcePath($path2)));
327
+            if ($stat1['dev'] !== $stat2['dev']) {
328
+                $result = $this->copy($path1, $path2);
329
+                if ($result) {
330
+                    $result &= $this->rmdir($path1);
331
+                }
332
+                return $result;
333
+            }
334
+
335
+            if ($this->treeContainsBlacklistedFile($this->getSourcePath($path1))) {
336
+                throw new ForbiddenException('Invalid path', false);
337
+            }
338
+        }
339
+
340
+        return rename($this->getSourcePath($path1), $this->getSourcePath($path2));
341
+    }
342
+
343
+    public function copy($path1, $path2) {
344
+        if ($this->is_dir($path1)) {
345
+            return parent::copy($path1, $path2);
346
+        } else {
347
+            return copy($this->getSourcePath($path1), $this->getSourcePath($path2));
348
+        }
349
+    }
350
+
351
+    public function fopen($path, $mode) {
352
+        return fopen($this->getSourcePath($path), $mode);
353
+    }
354
+
355
+    public function hash($type, $path, $raw = false) {
356
+        return hash_file($type, $this->getSourcePath($path), $raw);
357
+    }
358
+
359
+    public function free_space($path) {
360
+        $sourcePath = $this->getSourcePath($path);
361
+        // using !is_dir because $sourcePath might be a part file or
362
+        // non-existing file, so we'd still want to use the parent dir
363
+        // in such cases
364
+        if (!is_dir($sourcePath)) {
365
+            // disk_free_space doesn't work on files
366
+            $sourcePath = dirname($sourcePath);
367
+        }
368
+        $space = @disk_free_space($sourcePath);
369
+        if ($space === false || is_null($space)) {
370
+            return \OCP\Files\FileInfo::SPACE_UNKNOWN;
371
+        }
372
+        return $space;
373
+    }
374
+
375
+    public function search($query) {
376
+        return $this->searchInDir($query);
377
+    }
378
+
379
+    public function getLocalFile($path) {
380
+        return $this->getSourcePath($path);
381
+    }
382
+
383
+    public function getLocalFolder($path) {
384
+        return $this->getSourcePath($path);
385
+    }
386
+
387
+    /**
388
+     * @param string $query
389
+     * @param string $dir
390
+     * @return array
391
+     */
392
+    protected function searchInDir($query, $dir = '') {
393
+        $files = [];
394
+        $physicalDir = $this->getSourcePath($dir);
395
+        foreach (scandir($physicalDir) as $item) {
396
+            if (\OC\Files\Filesystem::isIgnoredDir($item))
397
+                continue;
398
+            $physicalItem = $physicalDir . '/' . $item;
399
+
400
+            if (strstr(strtolower($item), strtolower($query)) !== false) {
401
+                $files[] = $dir . '/' . $item;
402
+            }
403
+            if (is_dir($physicalItem)) {
404
+                $files = array_merge($files, $this->searchInDir($query, $dir . '/' . $item));
405
+            }
406
+        }
407
+        return $files;
408
+    }
409
+
410
+    /**
411
+     * check if a file or folder has been updated since $time
412
+     *
413
+     * @param string $path
414
+     * @param int $time
415
+     * @return bool
416
+     */
417
+    public function hasUpdated($path, $time) {
418
+        if ($this->file_exists($path)) {
419
+            return $this->filemtime($path) > $time;
420
+        } else {
421
+            return true;
422
+        }
423
+    }
424
+
425
+    /**
426
+     * Get the source path (on disk) of a given path
427
+     *
428
+     * @param string $path
429
+     * @return string
430
+     * @throws ForbiddenException
431
+     */
432
+    public function getSourcePath($path) {
433
+        if (Filesystem::isFileBlacklisted($path)) {
434
+            throw new ForbiddenException('Invalid path', false);
435
+        }
436
+
437
+        $fullPath = $this->datadir . $path;
438
+        $currentPath = $path;
439
+        if ($this->allowSymlinks || $currentPath === '') {
440
+            return $fullPath;
441
+        }
442
+        $pathToResolve = $fullPath;
443
+        $realPath = realpath($pathToResolve);
444
+        while ($realPath === false) { // for non existing files check the parent directory
445
+            $currentPath = dirname($currentPath);
446
+            if ($currentPath === '' || $currentPath === '.') {
447
+                return $fullPath;
448
+            }
449
+            $realPath = realpath($this->datadir . $currentPath);
450
+        }
451
+        if ($realPath) {
452
+            $realPath = $realPath . '/';
453
+        }
454
+        if (substr($realPath, 0, $this->dataDirLength) === $this->realDataDir) {
455
+            return $fullPath;
456
+        }
457
+
458
+        \OCP\Util::writeLog('core', "Following symlinks is not allowed ('$fullPath' -> '$realPath' not inside '{$this->realDataDir}')", ILogger::ERROR);
459
+        throw new ForbiddenException('Following symlinks is not allowed', false);
460
+    }
461
+
462
+    /**
463
+     * {@inheritdoc}
464
+     */
465
+    public function isLocal() {
466
+        return true;
467
+    }
468
+
469
+    /**
470
+     * get the ETag for a file or folder
471
+     *
472
+     * @param string $path
473
+     * @return string
474
+     */
475
+    public function getETag($path) {
476
+        return $this->calculateEtag($path, $this->stat($path));
477
+    }
478
+
479
+    private function calculateEtag(string $path, array $stat): string {
480
+        if ($stat['mode'] & 0x4000) { // is_dir
481
+            return parent::getETag($path);
482
+        } else {
483
+            if ($stat === false) {
484
+                return md5('');
485
+            }
486
+
487
+            $toHash = '';
488
+            if (isset($stat['mtime'])) {
489
+                $toHash .= $stat['mtime'];
490
+            }
491
+            if (isset($stat['ino'])) {
492
+                $toHash .= $stat['ino'];
493
+            }
494
+            if (isset($stat['dev'])) {
495
+                $toHash .= $stat['dev'];
496
+            }
497
+            if (isset($stat['size'])) {
498
+                $toHash .= $stat['size'];
499
+            }
500
+
501
+            return md5($toHash);
502
+        }
503
+    }
504
+
505
+    /**
506
+     * @param IStorage $sourceStorage
507
+     * @param string $sourceInternalPath
508
+     * @param string $targetInternalPath
509
+     * @param bool $preserveMtime
510
+     * @return bool
511
+     */
512
+    public function copyFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime = false) {
513
+        if ($sourceStorage->instanceOfStorage(Local::class)) {
514
+            if ($sourceStorage->instanceOfStorage(Jail::class)) {
515
+                /**
516
+                 * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
517
+                 */
518
+                $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
519
+            }
520
+            /**
521
+             * @var \OC\Files\Storage\Local $sourceStorage
522
+             */
523
+            $rootStorage = new Local(['datadir' => '/']);
524
+            return $rootStorage->copy($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
525
+        } else {
526
+            return parent::copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
527
+        }
528
+    }
529
+
530
+    /**
531
+     * @param IStorage $sourceStorage
532
+     * @param string $sourceInternalPath
533
+     * @param string $targetInternalPath
534
+     * @return bool
535
+     */
536
+    public function moveFromStorage(IStorage $sourceStorage, $sourceInternalPath, $targetInternalPath) {
537
+        if ($sourceStorage->instanceOfStorage(Local::class)) {
538
+            if ($sourceStorage->instanceOfStorage(Jail::class)) {
539
+                /**
540
+                 * @var \OC\Files\Storage\Wrapper\Jail $sourceStorage
541
+                 */
542
+                $sourceInternalPath = $sourceStorage->getUnjailedPath($sourceInternalPath);
543
+            }
544
+            /**
545
+             * @var \OC\Files\Storage\Local $sourceStorage
546
+             */
547
+            $rootStorage = new Local(['datadir' => '/']);
548
+            return $rootStorage->rename($sourceStorage->getSourcePath($sourceInternalPath), $this->getSourcePath($targetInternalPath));
549
+        } else {
550
+            return parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
551
+        }
552
+    }
553
+
554
+    public function writeStream(string $path, $stream, int $size = null): int {
555
+        return (int)file_put_contents($this->getSourcePath($path), $stream);
556
+    }
557 557
 }
Please login to merge, or discard this patch.