Completed
Push — master ( 6a16a2...464d12 )
by
unknown
22:58
created
lib/private/Files/Cache/Cache.php 2 patches
Indentation   +1260 added lines, -1260 removed lines patch added patch discarded remove patch
@@ -47,1264 +47,1264 @@
 block discarded – undo
47 47
  * - ChangePropagator: updates the mtime and etags of parent folders whenever a change to the cache is made to the cache by the updater
48 48
  */
49 49
 class Cache implements ICache {
50
-	use MoveFromCacheTrait {
51
-		MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
52
-	}
53
-
54
-	/**
55
-	 * @var array partial data for the cache
56
-	 */
57
-	protected array $partial = [];
58
-	protected string $storageId;
59
-	protected Storage $storageCache;
60
-	protected IMimeTypeLoader $mimetypeLoader;
61
-	protected IDBConnection $connection;
62
-	protected SystemConfig $systemConfig;
63
-	protected LoggerInterface $logger;
64
-	protected QuerySearchHelper $querySearchHelper;
65
-	protected IEventDispatcher $eventDispatcher;
66
-	protected IFilesMetadataManager $metadataManager;
67
-
68
-	public function __construct(
69
-		private IStorage $storage,
70
-		// this constructor is used in to many pleases to easily do proper di
71
-		// so instead we group it all together
72
-		?CacheDependencies $dependencies = null,
73
-	) {
74
-		$this->storageId = $storage->getId();
75
-		if (strlen($this->storageId) > 64) {
76
-			$this->storageId = md5($this->storageId);
77
-		}
78
-		if (!$dependencies) {
79
-			$dependencies = \OCP\Server::get(CacheDependencies::class);
80
-		}
81
-		$this->storageCache = new Storage($this->storage, true, $dependencies->getConnection());
82
-		$this->mimetypeLoader = $dependencies->getMimeTypeLoader();
83
-		$this->connection = $dependencies->getConnection();
84
-		$this->systemConfig = $dependencies->getSystemConfig();
85
-		$this->logger = $dependencies->getLogger();
86
-		$this->querySearchHelper = $dependencies->getQuerySearchHelper();
87
-		$this->eventDispatcher = $dependencies->getEventDispatcher();
88
-		$this->metadataManager = $dependencies->getMetadataManager();
89
-	}
90
-
91
-	protected function getQueryBuilder() {
92
-		return new CacheQueryBuilder(
93
-			$this->connection->getQueryBuilder(),
94
-			$this->metadataManager,
95
-		);
96
-	}
97
-
98
-	public function getStorageCache(): Storage {
99
-		return $this->storageCache;
100
-	}
101
-
102
-	/**
103
-	 * Get the numeric storage id for this cache's storage
104
-	 *
105
-	 * @return int
106
-	 */
107
-	public function getNumericStorageId() {
108
-		return $this->storageCache->getNumericId();
109
-	}
110
-
111
-	/**
112
-	 * get the stored metadata of a file or folder
113
-	 *
114
-	 * @param string|int $file either the path of a file or folder or the file id for a file or folder
115
-	 * @return ICacheEntry|false the cache entry as array or false if the file is not found in the cache
116
-	 */
117
-	public function get($file) {
118
-		$query = $this->getQueryBuilder();
119
-		$query->selectFileCache();
120
-		$metadataQuery = $query->selectMetadata();
121
-
122
-		if (is_string($file) || $file == '') {
123
-			// normalize file
124
-			$file = $this->normalize($file);
125
-
126
-			$query->wherePath($file);
127
-		} else { //file id
128
-			$query->whereFileId($file);
129
-		}
130
-		$query->whereStorageId($this->getNumericStorageId());
131
-
132
-		$result = $query->executeQuery();
133
-		$data = $result->fetch();
134
-		$result->closeCursor();
135
-
136
-		if ($data !== false) {
137
-			$data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
138
-			return self::cacheEntryFromData($data, $this->mimetypeLoader);
139
-		} else {
140
-			//merge partial data
141
-			if (is_string($file) && isset($this->partial[$file])) {
142
-				return $this->partial[$file];
143
-			}
144
-		}
145
-
146
-		return false;
147
-	}
148
-
149
-	/**
150
-	 * Create a CacheEntry from database row
151
-	 *
152
-	 * @param array $data
153
-	 * @param IMimeTypeLoader $mimetypeLoader
154
-	 * @return CacheEntry
155
-	 */
156
-	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
157
-		//fix types
158
-		$data['name'] = (string)$data['name'];
159
-		$data['path'] = (string)$data['path'];
160
-		$data['fileid'] = (int)$data['fileid'];
161
-		$data['parent'] = (int)$data['parent'];
162
-		$data['size'] = Util::numericToNumber($data['size']);
163
-		$data['unencrypted_size'] = Util::numericToNumber($data['unencrypted_size'] ?? 0);
164
-		$data['mtime'] = (int)$data['mtime'];
165
-		$data['storage_mtime'] = (int)$data['storage_mtime'];
166
-		$data['encryptedVersion'] = (int)$data['encrypted'];
167
-		$data['encrypted'] = (bool)$data['encrypted'];
168
-		$data['storage_id'] = $data['storage'];
169
-		$data['storage'] = (int)$data['storage'];
170
-		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
171
-		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
172
-		if ($data['storage_mtime'] == 0) {
173
-			$data['storage_mtime'] = $data['mtime'];
174
-		}
175
-		if (isset($data['f_permissions'])) {
176
-			$data['scan_permissions'] ??= $data['f_permissions'];
177
-		}
178
-		$data['permissions'] = (int)$data['permissions'];
179
-		if (isset($data['creation_time'])) {
180
-			$data['creation_time'] = (int)$data['creation_time'];
181
-		}
182
-		if (isset($data['upload_time'])) {
183
-			$data['upload_time'] = (int)$data['upload_time'];
184
-		}
185
-		return new CacheEntry($data);
186
-	}
187
-
188
-	/**
189
-	 * get the metadata of all files stored in $folder
190
-	 *
191
-	 * @param string $folder
192
-	 * @return ICacheEntry[]
193
-	 */
194
-	public function getFolderContents($folder) {
195
-		$fileId = $this->getId($folder);
196
-		return $this->getFolderContentsById($fileId);
197
-	}
198
-
199
-	/**
200
-	 * get the metadata of all files stored in $folder
201
-	 *
202
-	 * @param int $fileId the file id of the folder
203
-	 * @return ICacheEntry[]
204
-	 */
205
-	public function getFolderContentsById($fileId) {
206
-		if ($fileId > -1) {
207
-			$query = $this->getQueryBuilder();
208
-			$query->selectFileCache()
209
-				->whereParent($fileId)
210
-				->whereStorageId($this->getNumericStorageId())
211
-				->orderBy('name', 'ASC');
212
-
213
-			$metadataQuery = $query->selectMetadata();
214
-
215
-			$result = $query->executeQuery();
216
-			$files = $result->fetchAll();
217
-			$result->closeCursor();
218
-
219
-			return array_map(function (array $data) use ($metadataQuery) {
220
-				$data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
221
-				return self::cacheEntryFromData($data, $this->mimetypeLoader);
222
-			}, $files);
223
-		}
224
-		return [];
225
-	}
226
-
227
-	/**
228
-	 * insert or update meta data for a file or folder
229
-	 *
230
-	 * @param string $file
231
-	 * @param array $data
232
-	 *
233
-	 * @return int file id
234
-	 * @throws \RuntimeException
235
-	 */
236
-	public function put($file, array $data) {
237
-		if (($id = $this->getId($file)) > -1) {
238
-			$this->update($id, $data);
239
-			return $id;
240
-		} else {
241
-			return $this->insert($file, $data);
242
-		}
243
-	}
244
-
245
-	/**
246
-	 * insert meta data for a new file or folder
247
-	 *
248
-	 * @param string $file
249
-	 * @param array $data
250
-	 *
251
-	 * @return int file id
252
-	 * @throws \RuntimeException|Exception
253
-	 */
254
-	public function insert($file, array $data) {
255
-		// normalize file
256
-		$file = $this->normalize($file);
257
-
258
-		if (isset($this->partial[$file])) { //add any saved partial data
259
-			$data = array_merge($this->partial[$file]->getData(), $data);
260
-			unset($this->partial[$file]);
261
-		}
262
-
263
-		$requiredFields = ['size', 'mtime', 'mimetype'];
264
-		foreach ($requiredFields as $field) {
265
-			if (!isset($data[$field])) { //data not complete save as partial and return
266
-				$this->partial[$file] = new CacheEntry($data);
267
-				return -1;
268
-			}
269
-		}
270
-
271
-		$data['path'] = $file;
272
-		if (!isset($data['parent'])) {
273
-			$data['parent'] = $this->getParentId($file);
274
-		}
275
-		if ($data['parent'] === -1 && $file !== '') {
276
-			throw new \Exception('Parent folder not in filecache for ' . $file);
277
-		}
278
-		$data['name'] = basename($file);
279
-
280
-		[$values, $extensionValues] = $this->normalizeData($data);
281
-		$storageId = $this->getNumericStorageId();
282
-		$values['storage'] = $storageId;
283
-
284
-		try {
285
-			$builder = $this->connection->getQueryBuilder();
286
-			$builder->insert('filecache');
287
-
288
-			foreach ($values as $column => $value) {
289
-				$builder->setValue($column, $builder->createNamedParameter($value));
290
-			}
291
-
292
-			if ($builder->executeStatement()) {
293
-				$fileId = $builder->getLastInsertId();
294
-
295
-				if (count($extensionValues)) {
296
-					$query = $this->getQueryBuilder();
297
-					$query->insert('filecache_extended');
298
-					$query->hintShardKey('storage', $storageId);
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->executeStatement();
305
-				}
306
-
307
-				$event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId);
308
-				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
309
-				$this->eventDispatcher->dispatchTyped($event);
310
-				return $fileId;
311
-			}
312
-		} catch (Exception $e) {
313
-			if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
314
-				// entry exists already
315
-				if ($this->connection->inTransaction()) {
316
-					$this->connection->commit();
317
-					$this->connection->beginTransaction();
318
-				}
319
-			} else {
320
-				throw $e;
321
-			}
322
-		}
323
-
324
-		// The file was created in the meantime
325
-		if (($id = $this->getId($file)) > -1) {
326
-			$this->update($id, $data);
327
-			return $id;
328
-		} else {
329
-			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.');
330
-		}
331
-	}
332
-
333
-	/**
334
-	 * update the metadata of an existing file or folder in the cache
335
-	 *
336
-	 * @param int $id the fileid of the existing file or folder
337
-	 * @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
338
-	 */
339
-	public function update($id, array $data) {
340
-		if (isset($data['path'])) {
341
-			// normalize path
342
-			$data['path'] = $this->normalize($data['path']);
343
-		}
344
-
345
-		if (isset($data['name'])) {
346
-			// normalize path
347
-			$data['name'] = $this->normalize($data['name']);
348
-		}
349
-
350
-		[$values, $extensionValues] = $this->normalizeData($data);
351
-
352
-		if (count($values)) {
353
-			$query = $this->getQueryBuilder();
354
-
355
-			$query->update('filecache')
356
-				->whereFileId($id)
357
-				->whereStorageId($this->getNumericStorageId())
358
-				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
359
-					return $query->expr()->orX(
360
-						$query->expr()->neq($key, $query->createNamedParameter($value)),
361
-						$query->expr()->isNull($key)
362
-					);
363
-				}, array_keys($values), array_values($values))));
364
-
365
-			foreach ($values as $key => $value) {
366
-				$query->set($key, $query->createNamedParameter($value));
367
-			}
368
-
369
-			$query->executeStatement();
370
-		}
371
-
372
-		if (count($extensionValues)) {
373
-			try {
374
-				$query = $this->getQueryBuilder();
375
-				$query->insert('filecache_extended');
376
-				$query->hintShardKey('storage', $this->getNumericStorageId());
377
-
378
-				$query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
379
-				foreach ($extensionValues as $column => $value) {
380
-					$query->setValue($column, $query->createNamedParameter($value));
381
-				}
382
-
383
-				$query->executeStatement();
384
-			} catch (Exception $e) {
385
-				if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
386
-					throw $e;
387
-				}
388
-				$query = $this->getQueryBuilder();
389
-				$query->update('filecache_extended')
390
-					->whereFileId($id)
391
-					->hintShardKey('storage', $this->getNumericStorageId())
392
-					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
393
-						return $query->expr()->orX(
394
-							$query->expr()->neq($key, $query->createNamedParameter($value)),
395
-							$query->expr()->isNull($key)
396
-						);
397
-					}, array_keys($extensionValues), array_values($extensionValues))));
398
-
399
-				foreach ($extensionValues as $key => $value) {
400
-					$query->set($key, $query->createNamedParameter($value));
401
-				}
402
-
403
-				$query->executeStatement();
404
-			}
405
-		}
406
-
407
-		$path = $this->getPathById($id);
408
-		// path can still be null if the file doesn't exist
409
-		if ($path !== null) {
410
-			$event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId());
411
-			$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
412
-			$this->eventDispatcher->dispatchTyped($event);
413
-		}
414
-	}
415
-
416
-	/**
417
-	 * extract query parts and params array from data array
418
-	 *
419
-	 * @param array $data
420
-	 * @return array
421
-	 */
422
-	protected function normalizeData(array $data): array {
423
-		$fields = [
424
-			'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
425
-			'etag', 'permissions', 'checksum', 'storage', 'unencrypted_size'];
426
-		$extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
427
-
428
-		$doNotCopyStorageMTime = false;
429
-		if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
430
-			// this horrific magic tells it to not copy storage_mtime to mtime
431
-			unset($data['mtime']);
432
-			$doNotCopyStorageMTime = true;
433
-		}
434
-
435
-		$params = [];
436
-		$extensionParams = [];
437
-		foreach ($data as $name => $value) {
438
-			if (in_array($name, $fields)) {
439
-				if ($name === 'path') {
440
-					$params['path_hash'] = md5($value);
441
-				} elseif ($name === 'mimetype') {
442
-					$params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
443
-					$value = $this->mimetypeLoader->getId($value);
444
-				} elseif ($name === 'storage_mtime') {
445
-					if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
446
-						$params['mtime'] = $value;
447
-					}
448
-				} elseif ($name === 'encrypted') {
449
-					if (isset($data['encryptedVersion'])) {
450
-						$value = $data['encryptedVersion'];
451
-					} else {
452
-						// Boolean to integer conversion
453
-						$value = $value ? 1 : 0;
454
-					}
455
-				}
456
-				$params[$name] = $value;
457
-			}
458
-			if (in_array($name, $extensionFields)) {
459
-				$extensionParams[$name] = $value;
460
-			}
461
-		}
462
-		return [$params, array_filter($extensionParams)];
463
-	}
464
-
465
-	/**
466
-	 * get the file id for a file
467
-	 *
468
-	 * 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
469
-	 *
470
-	 * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
471
-	 *
472
-	 * @param string $file
473
-	 * @return int
474
-	 */
475
-	public function getId($file) {
476
-		// normalize file
477
-		$file = $this->normalize($file);
478
-
479
-		$query = $this->getQueryBuilder();
480
-		$query->select('fileid')
481
-			->from('filecache')
482
-			->whereStorageId($this->getNumericStorageId())
483
-			->wherePath($file);
484
-
485
-		$result = $query->executeQuery();
486
-		$id = $result->fetchOne();
487
-		$result->closeCursor();
488
-
489
-		return $id === false ? -1 : (int)$id;
490
-	}
491
-
492
-	/**
493
-	 * get the id of the parent folder of a file
494
-	 *
495
-	 * @param string $file
496
-	 * @return int
497
-	 */
498
-	public function getParentId($file) {
499
-		if ($file === '') {
500
-			return -1;
501
-		} else {
502
-			$parent = $this->getParentPath($file);
503
-			return (int)$this->getId($parent);
504
-		}
505
-	}
506
-
507
-	private function getParentPath($path) {
508
-		$parent = dirname($path);
509
-		if ($parent === '.') {
510
-			$parent = '';
511
-		}
512
-		return $parent;
513
-	}
514
-
515
-	/**
516
-	 * check if a file is available in the cache
517
-	 *
518
-	 * @param string $file
519
-	 * @return bool
520
-	 */
521
-	public function inCache($file) {
522
-		return $this->getId($file) != -1;
523
-	}
524
-
525
-	/**
526
-	 * remove a file or folder from the cache
527
-	 *
528
-	 * when removing a folder from the cache all files and folders inside the folder will be removed as well
529
-	 *
530
-	 * @param string $file
531
-	 */
532
-	public function remove($file) {
533
-		$entry = $this->get($file);
534
-
535
-		if ($entry instanceof ICacheEntry) {
536
-			$query = $this->getQueryBuilder();
537
-			$query->delete('filecache')
538
-				->whereStorageId($this->getNumericStorageId())
539
-				->whereFileId($entry->getId());
540
-			$query->executeStatement();
541
-
542
-			$query = $this->getQueryBuilder();
543
-			$query->delete('filecache_extended')
544
-				->whereFileId($entry->getId())
545
-				->hintShardKey('storage', $this->getNumericStorageId());
546
-			$query->executeStatement();
547
-
548
-			if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
549
-				$this->removeChildren($entry);
550
-			}
551
-
552
-			$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $entry->getPath(), $entry->getId(), $this->getNumericStorageId()));
553
-		}
554
-	}
555
-
556
-	/**
557
-	 * Remove all children of a folder
558
-	 *
559
-	 * @param ICacheEntry $entry the cache entry of the folder to remove the children of
560
-	 * @throws \OC\DatabaseException
561
-	 */
562
-	private function removeChildren(ICacheEntry $entry) {
563
-		$parentIds = [$entry->getId()];
564
-		$queue = [$entry->getId()];
565
-		$deletedIds = [];
566
-		$deletedPaths = [];
567
-
568
-		// we walk depth first through the file tree, removing all filecache_extended attributes while we walk
569
-		// and collecting all folder ids to later use to delete the filecache entries
570
-		while ($entryId = array_pop($queue)) {
571
-			$children = $this->getFolderContentsById($entryId);
572
-			$childIds = array_map(function (ICacheEntry $cacheEntry) {
573
-				return $cacheEntry->getId();
574
-			}, $children);
575
-			$childPaths = array_map(function (ICacheEntry $cacheEntry) {
576
-				return $cacheEntry->getPath();
577
-			}, $children);
578
-
579
-			foreach ($childIds as $childId) {
580
-				$deletedIds[] = $childId;
581
-			}
582
-
583
-			foreach ($childPaths as $childPath) {
584
-				$deletedPaths[] = $childPath;
585
-			}
586
-
587
-			$query = $this->getQueryBuilder();
588
-			$query->delete('filecache_extended')
589
-				->where($query->expr()->in('fileid', $query->createParameter('childIds')))
590
-				->hintShardKey('storage', $this->getNumericStorageId());
591
-
592
-			foreach (array_chunk($childIds, 1000) as $childIdChunk) {
593
-				$query->setParameter('childIds', $childIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
594
-				$query->executeStatement();
595
-			}
596
-
597
-			/** @var ICacheEntry[] $childFolders */
598
-			$childFolders = [];
599
-			foreach ($children as $child) {
600
-				if ($child->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
601
-					$childFolders[] = $child;
602
-				}
603
-			}
604
-			foreach ($childFolders as $folder) {
605
-				$parentIds[] = $folder->getId();
606
-				$queue[] = $folder->getId();
607
-			}
608
-		}
609
-
610
-		$query = $this->getQueryBuilder();
611
-		$query->delete('filecache')
612
-			->whereStorageId($this->getNumericStorageId())
613
-			->whereParentInParameter('parentIds');
614
-
615
-		// Sorting before chunking allows the db to find the entries close to each
616
-		// other in the index
617
-		sort($parentIds, SORT_NUMERIC);
618
-		foreach (array_chunk($parentIds, 1000) as $parentIdChunk) {
619
-			$query->setParameter('parentIds', $parentIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
620
-			$query->executeStatement();
621
-		}
622
-
623
-		foreach (array_combine($deletedIds, $deletedPaths) as $fileId => $filePath) {
624
-			$cacheEntryRemovedEvent = new CacheEntryRemovedEvent(
625
-				$this->storage,
626
-				$filePath,
627
-				$fileId,
628
-				$this->getNumericStorageId()
629
-			);
630
-			$this->eventDispatcher->dispatchTyped($cacheEntryRemovedEvent);
631
-		}
632
-	}
633
-
634
-	/**
635
-	 * Move a file or folder in the cache
636
-	 *
637
-	 * @param string $source
638
-	 * @param string $target
639
-	 */
640
-	public function move($source, $target) {
641
-		$this->moveFromCache($this, $source, $target);
642
-	}
643
-
644
-	/**
645
-	 * Get the storage id and path needed for a move
646
-	 *
647
-	 * @param string $path
648
-	 * @return array [$storageId, $internalPath]
649
-	 */
650
-	protected function getMoveInfo($path) {
651
-		return [$this->getNumericStorageId(), $path];
652
-	}
653
-
654
-	protected function hasEncryptionWrapper(): bool {
655
-		return $this->storage->instanceOfStorage(Encryption::class);
656
-	}
657
-
658
-	protected function shouldEncrypt(string $targetPath): bool {
659
-		if (!$this->storage->instanceOfStorage(Encryption::class)) {
660
-			return false;
661
-		}
662
-		return $this->storage->shouldEncrypt($targetPath);
663
-	}
664
-
665
-	/**
666
-	 * Move a file or folder in the cache
667
-	 *
668
-	 * @param ICache $sourceCache
669
-	 * @param string $sourcePath
670
-	 * @param string $targetPath
671
-	 * @throws \OC\DatabaseException
672
-	 * @throws \Exception if the given storages have an invalid id
673
-	 */
674
-	public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
675
-		if ($sourceCache instanceof Cache) {
676
-			// normalize source and target
677
-			$sourcePath = $this->normalize($sourcePath);
678
-			$targetPath = $this->normalize($targetPath);
679
-
680
-			$sourceData = $sourceCache->get($sourcePath);
681
-			if (!$sourceData) {
682
-				throw new \Exception('Source path not found in cache: ' . $sourcePath);
683
-			}
684
-
685
-			$shardDefinition = $this->connection->getShardDefinition('filecache');
686
-			if (
687
-				$shardDefinition
688
-				&& $shardDefinition->getShardForKey($sourceCache->getNumericStorageId()) !== $shardDefinition->getShardForKey($this->getNumericStorageId())
689
-			) {
690
-				$this->moveFromStorageSharded($shardDefinition, $sourceCache, $sourceData, $targetPath);
691
-				return;
692
-			}
693
-
694
-			$sourceId = $sourceData['fileid'];
695
-			$newParentId = $this->getParentId($targetPath);
696
-
697
-			[$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
698
-			[$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
699
-
700
-			if (is_null($sourceStorageId) || $sourceStorageId === false) {
701
-				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
702
-			}
703
-			if (is_null($targetStorageId) || $targetStorageId === false) {
704
-				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
705
-			}
706
-
707
-			if ($sourceData['mimetype'] === 'httpd/unix-directory') {
708
-				//update all child entries
709
-				$sourceLength = mb_strlen($sourcePath);
710
-
711
-				$childIds = $this->getChildIds($sourceStorageId, $sourcePath);
712
-
713
-				$childChunks = array_chunk($childIds, 1000);
714
-
715
-				$query = $this->getQueryBuilder();
716
-
717
-				$fun = $query->func();
718
-				$newPathFunction = $fun->concat(
719
-					$query->createNamedParameter($targetPath),
720
-					$fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
721
-				);
722
-				$query->update('filecache')
723
-					->set('path_hash', $fun->md5($newPathFunction))
724
-					->set('path', $newPathFunction)
725
-					->whereStorageId($sourceStorageId)
726
-					->andWhere($query->expr()->in('fileid', $query->createParameter('files')));
727
-
728
-				if ($sourceStorageId !== $targetStorageId) {
729
-					$query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
730
-				}
731
-
732
-				// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
733
-				if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
734
-					$query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
735
-				}
736
-
737
-				// Retry transaction in case of RetryableException like deadlocks.
738
-				// Retry up to 4 times because we should receive up to 4 concurrent requests from the frontend
739
-				$retryLimit = 4;
740
-				for ($i = 1; $i <= $retryLimit; $i++) {
741
-					try {
742
-						$this->connection->beginTransaction();
743
-						foreach ($childChunks as $chunk) {
744
-							$query->setParameter('files', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
745
-							$query->executeStatement();
746
-						}
747
-						break;
748
-					} catch (\OC\DatabaseException $e) {
749
-						$this->connection->rollBack();
750
-						throw $e;
751
-					} catch (DbalException $e) {
752
-						$this->connection->rollBack();
753
-
754
-						if (!$e->isRetryable()) {
755
-							throw $e;
756
-						}
757
-
758
-						// Simply throw if we already retried 4 times.
759
-						if ($i === $retryLimit) {
760
-							throw $e;
761
-						}
762
-
763
-						// Sleep a bit to give some time to the other transaction to finish.
764
-						usleep(100 * 1000 * $i);
765
-					}
766
-				}
767
-			} else {
768
-				$this->connection->beginTransaction();
769
-			}
770
-
771
-			$query = $this->getQueryBuilder();
772
-			$query->update('filecache')
773
-				->set('path', $query->createNamedParameter($targetPath))
774
-				->set('path_hash', $query->createNamedParameter(md5($targetPath)))
775
-				->set('name', $query->createNamedParameter(basename($targetPath)))
776
-				->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
777
-				->whereStorageId($sourceStorageId)
778
-				->whereFileId($sourceId);
779
-
780
-			if ($sourceStorageId !== $targetStorageId) {
781
-				$query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
782
-			}
783
-
784
-			// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
785
-			if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
786
-				$query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
787
-			}
788
-
789
-			$query->executeStatement();
790
-
791
-			$this->connection->commit();
792
-
793
-			if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
794
-				$this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
795
-				$event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
796
-				$this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
797
-				$this->eventDispatcher->dispatchTyped($event);
798
-			} else {
799
-				$event = new CacheEntryUpdatedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
800
-				$this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
801
-				$this->eventDispatcher->dispatchTyped($event);
802
-			}
803
-		} else {
804
-			$this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
805
-		}
806
-	}
807
-
808
-	private function getChildIds(int $storageId, string $path): array {
809
-		$query = $this->connection->getQueryBuilder();
810
-		$query->select('fileid')
811
-			->from('filecache')
812
-			->where($query->expr()->eq('storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
813
-			->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($path) . '/%')));
814
-		return $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
815
-	}
816
-
817
-	/**
818
-	 * remove all entries for files that are stored on the storage from the cache
819
-	 */
820
-	public function clear() {
821
-		$query = $this->getQueryBuilder();
822
-		$query->delete('filecache')
823
-			->whereStorageId($this->getNumericStorageId());
824
-		$query->executeStatement();
825
-
826
-		$query = $this->connection->getQueryBuilder();
827
-		$query->delete('storages')
828
-			->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
829
-		$query->executeStatement();
830
-	}
831
-
832
-	/**
833
-	 * Get the scan status of a file
834
-	 *
835
-	 * - Cache::NOT_FOUND: File is not in the cache
836
-	 * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
837
-	 * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
838
-	 * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
839
-	 *
840
-	 * @param string $file
841
-	 *
842
-	 * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
843
-	 */
844
-	public function getStatus($file) {
845
-		// normalize file
846
-		$file = $this->normalize($file);
847
-
848
-		$query = $this->getQueryBuilder();
849
-		$query->select('size')
850
-			->from('filecache')
851
-			->whereStorageId($this->getNumericStorageId())
852
-			->wherePath($file);
853
-
854
-		$result = $query->executeQuery();
855
-		$size = $result->fetchOne();
856
-		$result->closeCursor();
857
-
858
-		if ($size !== false) {
859
-			if ((int)$size === -1) {
860
-				return self::SHALLOW;
861
-			} else {
862
-				return self::COMPLETE;
863
-			}
864
-		} else {
865
-			if (isset($this->partial[$file])) {
866
-				return self::PARTIAL;
867
-			} else {
868
-				return self::NOT_FOUND;
869
-			}
870
-		}
871
-	}
872
-
873
-	/**
874
-	 * search for files matching $pattern
875
-	 *
876
-	 * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
877
-	 * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
878
-	 */
879
-	public function search($pattern) {
880
-		$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', $pattern);
881
-		return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
882
-	}
883
-
884
-	/**
885
-	 * search for files by mimetype
886
-	 *
887
-	 * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
888
-	 *                         where it will search for all mimetypes in the group ('image/*')
889
-	 * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
890
-	 */
891
-	public function searchByMime($mimetype) {
892
-		if (!str_contains($mimetype, '/')) {
893
-			$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%');
894
-		} else {
895
-			$operator = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype);
896
-		}
897
-		return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
898
-	}
899
-
900
-	public function searchQuery(ISearchQuery $query) {
901
-		return current($this->querySearchHelper->searchInCaches($query, [$this]));
902
-	}
903
-
904
-	/**
905
-	 * Re-calculate the folder size and the size of all parent folders
906
-	 *
907
-	 * @param array|ICacheEntry|null $data (optional) meta data of the folder
908
-	 */
909
-	public function correctFolderSize(string $path, $data = null, bool $isBackgroundScan = false): void {
910
-		$this->calculateFolderSize($path, $data);
911
-
912
-		if ($path !== '') {
913
-			$parent = dirname($path);
914
-			if ($parent === '.' || $parent === '/') {
915
-				$parent = '';
916
-			}
917
-
918
-			if ($isBackgroundScan) {
919
-				$parentData = $this->get($parent);
920
-				if ($parentData !== false
921
-					&& $parentData['size'] !== -1
922
-					&& $this->getIncompleteChildrenCount($parentData['fileid']) === 0
923
-				) {
924
-					$this->correctFolderSize($parent, $parentData, $isBackgroundScan);
925
-				}
926
-			} else {
927
-				$this->correctFolderSize($parent);
928
-			}
929
-		}
930
-	}
931
-
932
-	/**
933
-	 * get the incomplete count that shares parent $folder
934
-	 *
935
-	 * @param int $fileId the file id of the folder
936
-	 * @return int
937
-	 */
938
-	public function getIncompleteChildrenCount($fileId) {
939
-		if ($fileId > -1) {
940
-			$query = $this->getQueryBuilder();
941
-			$query->select($query->func()->count())
942
-				->from('filecache')
943
-				->whereParent($fileId)
944
-				->whereStorageId($this->getNumericStorageId())
945
-				->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)));
946
-
947
-			$result = $query->executeQuery();
948
-			$size = (int)$result->fetchOne();
949
-			$result->closeCursor();
950
-
951
-			return $size;
952
-		}
953
-		return -1;
954
-	}
955
-
956
-	/**
957
-	 * calculate the size of a folder and set it in the cache
958
-	 *
959
-	 * @param string $path
960
-	 * @param array|null|ICacheEntry $entry (optional) meta data of the folder
961
-	 * @return int|float
962
-	 */
963
-	public function calculateFolderSize($path, $entry = null) {
964
-		return $this->calculateFolderSizeInner($path, $entry);
965
-	}
966
-
967
-
968
-	/**
969
-	 * inner function because we can't add new params to the public function without breaking any child classes
970
-	 *
971
-	 * @param string $path
972
-	 * @param array|null|ICacheEntry $entry (optional) meta data of the folder
973
-	 * @param bool $ignoreUnknown don't mark the folder size as unknown if any of it's children are unknown
974
-	 * @return int|float
975
-	 */
976
-	protected function calculateFolderSizeInner(string $path, $entry = null, bool $ignoreUnknown = false) {
977
-		$totalSize = 0;
978
-		if (is_null($entry) || !isset($entry['fileid'])) {
979
-			$entry = $this->get($path);
980
-		}
981
-		if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
982
-			$id = $entry['fileid'];
983
-
984
-			$query = $this->getQueryBuilder();
985
-			$query->select('size', 'unencrypted_size')
986
-				->from('filecache')
987
-				->whereStorageId($this->getNumericStorageId())
988
-				->whereParent($id);
989
-			if ($ignoreUnknown) {
990
-				$query->andWhere($query->expr()->gte('size', $query->createNamedParameter(0)));
991
-			}
992
-
993
-			$result = $query->executeQuery();
994
-			$rows = $result->fetchAll();
995
-			$result->closeCursor();
996
-
997
-			if ($rows) {
998
-				$sizes = array_map(function (array $row) {
999
-					return Util::numericToNumber($row['size']);
1000
-				}, $rows);
1001
-				$unencryptedOnlySizes = array_map(function (array $row) {
1002
-					return Util::numericToNumber($row['unencrypted_size']);
1003
-				}, $rows);
1004
-				$unencryptedSizes = array_map(function (array $row) {
1005
-					return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
1006
-				}, $rows);
1007
-
1008
-				$sum = array_sum($sizes);
1009
-				$min = min($sizes);
1010
-
1011
-				$unencryptedSum = array_sum($unencryptedSizes);
1012
-				$unencryptedMin = min($unencryptedSizes);
1013
-				$unencryptedMax = max($unencryptedOnlySizes);
1014
-
1015
-				$sum = 0 + $sum;
1016
-				$min = 0 + $min;
1017
-				if ($min === -1) {
1018
-					$totalSize = $min;
1019
-				} else {
1020
-					$totalSize = $sum;
1021
-				}
1022
-				if ($unencryptedMin === -1 || $min === -1) {
1023
-					$unencryptedTotal = $unencryptedMin;
1024
-				} else {
1025
-					$unencryptedTotal = $unencryptedSum;
1026
-				}
1027
-			} else {
1028
-				$totalSize = 0;
1029
-				$unencryptedTotal = 0;
1030
-				$unencryptedMax = 0;
1031
-			}
1032
-
1033
-			// only set unencrypted size for a folder if any child entries have it set, or the folder is empty
1034
-			$shouldWriteUnEncryptedSize = $unencryptedMax > 0 || $totalSize === 0 || ($entry['unencrypted_size'] ?? 0) > 0;
1035
-			if ($entry['size'] !== $totalSize || (($entry['unencrypted_size'] ?? 0) !== $unencryptedTotal && $shouldWriteUnEncryptedSize)) {
1036
-				if ($shouldWriteUnEncryptedSize) {
1037
-					// if all children have an unencrypted size of 0, just set the folder unencrypted size to 0 instead of summing the sizes
1038
-					if ($unencryptedMax === 0) {
1039
-						$unencryptedTotal = 0;
1040
-					}
1041
-
1042
-					$this->update($id, [
1043
-						'size' => $totalSize,
1044
-						'unencrypted_size' => $unencryptedTotal,
1045
-					]);
1046
-				} else {
1047
-					$this->update($id, [
1048
-						'size' => $totalSize,
1049
-					]);
1050
-				}
1051
-			}
1052
-		}
1053
-		return $totalSize;
1054
-	}
1055
-
1056
-	/**
1057
-	 * get all file ids on the files on the storage
1058
-	 *
1059
-	 * @return int[]
1060
-	 */
1061
-	public function getAll() {
1062
-		$query = $this->getQueryBuilder();
1063
-		$query->select('fileid')
1064
-			->from('filecache')
1065
-			->whereStorageId($this->getNumericStorageId());
1066
-
1067
-		$result = $query->executeQuery();
1068
-		$files = $result->fetchAll(\PDO::FETCH_COLUMN);
1069
-		$result->closeCursor();
1070
-
1071
-		return array_map(function ($id) {
1072
-			return (int)$id;
1073
-		}, $files);
1074
-	}
1075
-
1076
-	/**
1077
-	 * find a folder in the cache which has not been fully scanned
1078
-	 *
1079
-	 * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
1080
-	 * use the one with the highest id gives the best result with the background scanner, since that is most
1081
-	 * likely the folder where we stopped scanning previously
1082
-	 *
1083
-	 * @return string|false the path of the folder or false when no folder matched
1084
-	 */
1085
-	public function getIncomplete() {
1086
-		$query = $this->getQueryBuilder();
1087
-		$query->select('path')
1088
-			->from('filecache')
1089
-			->whereStorageId($this->getNumericStorageId())
1090
-			->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
1091
-			->orderBy('fileid', 'DESC')
1092
-			->setMaxResults(1);
1093
-
1094
-		$result = $query->executeQuery();
1095
-		$path = $result->fetchOne();
1096
-		$result->closeCursor();
1097
-
1098
-		return $path === false ? false : (string)$path;
1099
-	}
1100
-
1101
-	/**
1102
-	 * get the path of a file on this storage by it's file id
1103
-	 *
1104
-	 * @param int $id the file id of the file or folder to search
1105
-	 * @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
1106
-	 */
1107
-	public function getPathById($id) {
1108
-		$query = $this->getQueryBuilder();
1109
-		$query->select('path')
1110
-			->from('filecache')
1111
-			->whereStorageId($this->getNumericStorageId())
1112
-			->whereFileId($id);
1113
-
1114
-		$result = $query->executeQuery();
1115
-		$path = $result->fetchOne();
1116
-		$result->closeCursor();
1117
-
1118
-		if ($path === false) {
1119
-			return null;
1120
-		}
1121
-
1122
-		return (string)$path;
1123
-	}
1124
-
1125
-	/**
1126
-	 * get the storage id of the storage for a file and the internal path of the file
1127
-	 * unlike getPathById this does not limit the search to files on this storage and
1128
-	 * instead does a global search in the cache table
1129
-	 *
1130
-	 * @param int $id
1131
-	 * @return array first element holding the storage id, second the path
1132
-	 * @deprecated 17.0.0 use getPathById() instead
1133
-	 */
1134
-	public static function getById($id) {
1135
-		$query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
1136
-		$query->select('path', 'storage')
1137
-			->from('filecache')
1138
-			->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
1139
-
1140
-		$result = $query->executeQuery();
1141
-		$row = $result->fetch();
1142
-		$result->closeCursor();
1143
-
1144
-		if ($row) {
1145
-			$numericId = $row['storage'];
1146
-			$path = $row['path'];
1147
-		} else {
1148
-			return null;
1149
-		}
1150
-
1151
-		if ($id = Storage::getStorageId($numericId)) {
1152
-			return [$id, $path];
1153
-		} else {
1154
-			return null;
1155
-		}
1156
-	}
1157
-
1158
-	/**
1159
-	 * normalize the given path
1160
-	 *
1161
-	 * @param string $path
1162
-	 * @return string
1163
-	 */
1164
-	public function normalize($path) {
1165
-		return trim(\OC_Util::normalizeUnicode($path), '/');
1166
-	}
1167
-
1168
-	/**
1169
-	 * Copy a file or folder in the cache
1170
-	 *
1171
-	 * @param ICache $sourceCache
1172
-	 * @param ICacheEntry $sourceEntry
1173
-	 * @param string $targetPath
1174
-	 * @return int fileId of copied entry
1175
-	 */
1176
-	public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
1177
-		if ($sourceEntry->getId() < 0) {
1178
-			throw new \RuntimeException('Invalid source cache entry on copyFromCache');
1179
-		}
1180
-		$data = $this->cacheEntryToArray($sourceEntry);
1181
-
1182
-		// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1183
-		if ($sourceCache instanceof Cache
1184
-			&& $sourceCache->hasEncryptionWrapper()
1185
-			&& !$this->shouldEncrypt($targetPath)) {
1186
-			$data['encrypted'] = 0;
1187
-		}
1188
-
1189
-		$fileId = $this->put($targetPath, $data);
1190
-		if ($fileId <= 0) {
1191
-			throw new \RuntimeException('Failed to copy to ' . $targetPath . ' from cache with source data ' . json_encode($data) . ' ');
1192
-		}
1193
-		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1194
-			$folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1195
-			foreach ($folderContent as $subEntry) {
1196
-				$subTargetPath = $targetPath . '/' . $subEntry->getName();
1197
-				$this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1198
-			}
1199
-		}
1200
-		return $fileId;
1201
-	}
1202
-
1203
-	private function cacheEntryToArray(ICacheEntry $entry): array {
1204
-		$data = [
1205
-			'size' => $entry->getSize(),
1206
-			'mtime' => $entry->getMTime(),
1207
-			'storage_mtime' => $entry->getStorageMTime(),
1208
-			'mimetype' => $entry->getMimeType(),
1209
-			'mimepart' => $entry->getMimePart(),
1210
-			'etag' => $entry->getEtag(),
1211
-			'permissions' => $entry->getPermissions(),
1212
-			'encrypted' => $entry->isEncrypted(),
1213
-			'creation_time' => $entry->getCreationTime(),
1214
-			'upload_time' => $entry->getUploadTime(),
1215
-			'metadata_etag' => $entry->getMetadataEtag(),
1216
-		];
1217
-		if ($entry instanceof CacheEntry && isset($entry['scan_permissions'])) {
1218
-			$data['permissions'] = $entry['scan_permissions'];
1219
-		}
1220
-		return $data;
1221
-	}
1222
-
1223
-	public function getQueryFilterForStorage(): ISearchOperator {
1224
-		return new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', $this->getNumericStorageId());
1225
-	}
1226
-
1227
-	public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
1228
-		if ($rawEntry->getStorageId() === $this->getNumericStorageId()) {
1229
-			return $rawEntry;
1230
-		} else {
1231
-			return null;
1232
-		}
1233
-	}
1234
-
1235
-	private function moveFromStorageSharded(ShardDefinition $shardDefinition, ICache $sourceCache, ICacheEntry $sourceEntry, $targetPath): void {
1236
-		$sourcePath = $sourceEntry->getPath();
1237
-		while ($sourceCache instanceof CacheWrapper) {
1238
-			if ($sourceCache instanceof CacheJail) {
1239
-				$sourcePath = $sourceCache->getSourcePath($sourcePath);
1240
-			}
1241
-			$sourceCache = $sourceCache->getCache();
1242
-		}
1243
-
1244
-		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1245
-			$fileIds = $this->getChildIds($sourceCache->getNumericStorageId(), $sourcePath);
1246
-		} else {
1247
-			$fileIds = [];
1248
-		}
1249
-		$fileIds[] = $sourceEntry->getId();
1250
-
1251
-		$helper = $this->connection->getCrossShardMoveHelper();
1252
-
1253
-		$sourceConnection = $helper->getConnection($shardDefinition, $sourceCache->getNumericStorageId());
1254
-		$targetConnection = $helper->getConnection($shardDefinition, $this->getNumericStorageId());
1255
-
1256
-		$cacheItems = $helper->loadItems($sourceConnection, 'filecache', 'fileid', $fileIds);
1257
-		$extendedItems = $helper->loadItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
1258
-		$metadataItems = $helper->loadItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
1259
-
1260
-		// when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1261
-		$removeEncryptedFlag = ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper()) && !$this->hasEncryptionWrapper();
1262
-
1263
-		$sourcePathLength = strlen($sourcePath);
1264
-		foreach ($cacheItems as &$cacheItem) {
1265
-			if ($cacheItem['path'] === $sourcePath) {
1266
-				$cacheItem['path'] = $targetPath;
1267
-				$cacheItem['parent'] = $this->getParentId($targetPath);
1268
-				$cacheItem['name'] = basename($cacheItem['path']);
1269
-			} else {
1270
-				$cacheItem['path'] = $targetPath . '/' . substr($cacheItem['path'], $sourcePathLength + 1); // +1 for the leading slash
1271
-			}
1272
-			$cacheItem['path_hash'] = md5($cacheItem['path']);
1273
-			$cacheItem['storage'] = $this->getNumericStorageId();
1274
-			if ($removeEncryptedFlag) {
1275
-				$cacheItem['encrypted'] = 0;
1276
-			}
1277
-		}
1278
-
1279
-		$targetConnection->beginTransaction();
1280
-
1281
-		try {
1282
-			$helper->saveItems($targetConnection, 'filecache', $cacheItems);
1283
-			$helper->saveItems($targetConnection, 'filecache_extended', $extendedItems);
1284
-			$helper->saveItems($targetConnection, 'files_metadata', $metadataItems);
1285
-		} catch (\Exception $e) {
1286
-			$targetConnection->rollback();
1287
-			throw $e;
1288
-		}
1289
-
1290
-		$sourceConnection->beginTransaction();
1291
-
1292
-		try {
1293
-			$helper->deleteItems($sourceConnection, 'filecache', 'fileid', $fileIds);
1294
-			$helper->deleteItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
1295
-			$helper->deleteItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
1296
-		} catch (\Exception $e) {
1297
-			$targetConnection->rollback();
1298
-			$sourceConnection->rollBack();
1299
-			throw $e;
1300
-		}
1301
-
1302
-		try {
1303
-			$sourceConnection->commit();
1304
-		} catch (\Exception $e) {
1305
-			$targetConnection->rollback();
1306
-			throw $e;
1307
-		}
1308
-		$targetConnection->commit();
1309
-	}
50
+    use MoveFromCacheTrait {
51
+        MoveFromCacheTrait::moveFromCache as moveFromCacheFallback;
52
+    }
53
+
54
+    /**
55
+     * @var array partial data for the cache
56
+     */
57
+    protected array $partial = [];
58
+    protected string $storageId;
59
+    protected Storage $storageCache;
60
+    protected IMimeTypeLoader $mimetypeLoader;
61
+    protected IDBConnection $connection;
62
+    protected SystemConfig $systemConfig;
63
+    protected LoggerInterface $logger;
64
+    protected QuerySearchHelper $querySearchHelper;
65
+    protected IEventDispatcher $eventDispatcher;
66
+    protected IFilesMetadataManager $metadataManager;
67
+
68
+    public function __construct(
69
+        private IStorage $storage,
70
+        // this constructor is used in to many pleases to easily do proper di
71
+        // so instead we group it all together
72
+        ?CacheDependencies $dependencies = null,
73
+    ) {
74
+        $this->storageId = $storage->getId();
75
+        if (strlen($this->storageId) > 64) {
76
+            $this->storageId = md5($this->storageId);
77
+        }
78
+        if (!$dependencies) {
79
+            $dependencies = \OCP\Server::get(CacheDependencies::class);
80
+        }
81
+        $this->storageCache = new Storage($this->storage, true, $dependencies->getConnection());
82
+        $this->mimetypeLoader = $dependencies->getMimeTypeLoader();
83
+        $this->connection = $dependencies->getConnection();
84
+        $this->systemConfig = $dependencies->getSystemConfig();
85
+        $this->logger = $dependencies->getLogger();
86
+        $this->querySearchHelper = $dependencies->getQuerySearchHelper();
87
+        $this->eventDispatcher = $dependencies->getEventDispatcher();
88
+        $this->metadataManager = $dependencies->getMetadataManager();
89
+    }
90
+
91
+    protected function getQueryBuilder() {
92
+        return new CacheQueryBuilder(
93
+            $this->connection->getQueryBuilder(),
94
+            $this->metadataManager,
95
+        );
96
+    }
97
+
98
+    public function getStorageCache(): Storage {
99
+        return $this->storageCache;
100
+    }
101
+
102
+    /**
103
+     * Get the numeric storage id for this cache's storage
104
+     *
105
+     * @return int
106
+     */
107
+    public function getNumericStorageId() {
108
+        return $this->storageCache->getNumericId();
109
+    }
110
+
111
+    /**
112
+     * get the stored metadata of a file or folder
113
+     *
114
+     * @param string|int $file either the path of a file or folder or the file id for a file or folder
115
+     * @return ICacheEntry|false the cache entry as array or false if the file is not found in the cache
116
+     */
117
+    public function get($file) {
118
+        $query = $this->getQueryBuilder();
119
+        $query->selectFileCache();
120
+        $metadataQuery = $query->selectMetadata();
121
+
122
+        if (is_string($file) || $file == '') {
123
+            // normalize file
124
+            $file = $this->normalize($file);
125
+
126
+            $query->wherePath($file);
127
+        } else { //file id
128
+            $query->whereFileId($file);
129
+        }
130
+        $query->whereStorageId($this->getNumericStorageId());
131
+
132
+        $result = $query->executeQuery();
133
+        $data = $result->fetch();
134
+        $result->closeCursor();
135
+
136
+        if ($data !== false) {
137
+            $data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
138
+            return self::cacheEntryFromData($data, $this->mimetypeLoader);
139
+        } else {
140
+            //merge partial data
141
+            if (is_string($file) && isset($this->partial[$file])) {
142
+                return $this->partial[$file];
143
+            }
144
+        }
145
+
146
+        return false;
147
+    }
148
+
149
+    /**
150
+     * Create a CacheEntry from database row
151
+     *
152
+     * @param array $data
153
+     * @param IMimeTypeLoader $mimetypeLoader
154
+     * @return CacheEntry
155
+     */
156
+    public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
157
+        //fix types
158
+        $data['name'] = (string)$data['name'];
159
+        $data['path'] = (string)$data['path'];
160
+        $data['fileid'] = (int)$data['fileid'];
161
+        $data['parent'] = (int)$data['parent'];
162
+        $data['size'] = Util::numericToNumber($data['size']);
163
+        $data['unencrypted_size'] = Util::numericToNumber($data['unencrypted_size'] ?? 0);
164
+        $data['mtime'] = (int)$data['mtime'];
165
+        $data['storage_mtime'] = (int)$data['storage_mtime'];
166
+        $data['encryptedVersion'] = (int)$data['encrypted'];
167
+        $data['encrypted'] = (bool)$data['encrypted'];
168
+        $data['storage_id'] = $data['storage'];
169
+        $data['storage'] = (int)$data['storage'];
170
+        $data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
171
+        $data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
172
+        if ($data['storage_mtime'] == 0) {
173
+            $data['storage_mtime'] = $data['mtime'];
174
+        }
175
+        if (isset($data['f_permissions'])) {
176
+            $data['scan_permissions'] ??= $data['f_permissions'];
177
+        }
178
+        $data['permissions'] = (int)$data['permissions'];
179
+        if (isset($data['creation_time'])) {
180
+            $data['creation_time'] = (int)$data['creation_time'];
181
+        }
182
+        if (isset($data['upload_time'])) {
183
+            $data['upload_time'] = (int)$data['upload_time'];
184
+        }
185
+        return new CacheEntry($data);
186
+    }
187
+
188
+    /**
189
+     * get the metadata of all files stored in $folder
190
+     *
191
+     * @param string $folder
192
+     * @return ICacheEntry[]
193
+     */
194
+    public function getFolderContents($folder) {
195
+        $fileId = $this->getId($folder);
196
+        return $this->getFolderContentsById($fileId);
197
+    }
198
+
199
+    /**
200
+     * get the metadata of all files stored in $folder
201
+     *
202
+     * @param int $fileId the file id of the folder
203
+     * @return ICacheEntry[]
204
+     */
205
+    public function getFolderContentsById($fileId) {
206
+        if ($fileId > -1) {
207
+            $query = $this->getQueryBuilder();
208
+            $query->selectFileCache()
209
+                ->whereParent($fileId)
210
+                ->whereStorageId($this->getNumericStorageId())
211
+                ->orderBy('name', 'ASC');
212
+
213
+            $metadataQuery = $query->selectMetadata();
214
+
215
+            $result = $query->executeQuery();
216
+            $files = $result->fetchAll();
217
+            $result->closeCursor();
218
+
219
+            return array_map(function (array $data) use ($metadataQuery) {
220
+                $data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
221
+                return self::cacheEntryFromData($data, $this->mimetypeLoader);
222
+            }, $files);
223
+        }
224
+        return [];
225
+    }
226
+
227
+    /**
228
+     * insert or update meta data for a file or folder
229
+     *
230
+     * @param string $file
231
+     * @param array $data
232
+     *
233
+     * @return int file id
234
+     * @throws \RuntimeException
235
+     */
236
+    public function put($file, array $data) {
237
+        if (($id = $this->getId($file)) > -1) {
238
+            $this->update($id, $data);
239
+            return $id;
240
+        } else {
241
+            return $this->insert($file, $data);
242
+        }
243
+    }
244
+
245
+    /**
246
+     * insert meta data for a new file or folder
247
+     *
248
+     * @param string $file
249
+     * @param array $data
250
+     *
251
+     * @return int file id
252
+     * @throws \RuntimeException|Exception
253
+     */
254
+    public function insert($file, array $data) {
255
+        // normalize file
256
+        $file = $this->normalize($file);
257
+
258
+        if (isset($this->partial[$file])) { //add any saved partial data
259
+            $data = array_merge($this->partial[$file]->getData(), $data);
260
+            unset($this->partial[$file]);
261
+        }
262
+
263
+        $requiredFields = ['size', 'mtime', 'mimetype'];
264
+        foreach ($requiredFields as $field) {
265
+            if (!isset($data[$field])) { //data not complete save as partial and return
266
+                $this->partial[$file] = new CacheEntry($data);
267
+                return -1;
268
+            }
269
+        }
270
+
271
+        $data['path'] = $file;
272
+        if (!isset($data['parent'])) {
273
+            $data['parent'] = $this->getParentId($file);
274
+        }
275
+        if ($data['parent'] === -1 && $file !== '') {
276
+            throw new \Exception('Parent folder not in filecache for ' . $file);
277
+        }
278
+        $data['name'] = basename($file);
279
+
280
+        [$values, $extensionValues] = $this->normalizeData($data);
281
+        $storageId = $this->getNumericStorageId();
282
+        $values['storage'] = $storageId;
283
+
284
+        try {
285
+            $builder = $this->connection->getQueryBuilder();
286
+            $builder->insert('filecache');
287
+
288
+            foreach ($values as $column => $value) {
289
+                $builder->setValue($column, $builder->createNamedParameter($value));
290
+            }
291
+
292
+            if ($builder->executeStatement()) {
293
+                $fileId = $builder->getLastInsertId();
294
+
295
+                if (count($extensionValues)) {
296
+                    $query = $this->getQueryBuilder();
297
+                    $query->insert('filecache_extended');
298
+                    $query->hintShardKey('storage', $storageId);
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->executeStatement();
305
+                }
306
+
307
+                $event = new CacheEntryInsertedEvent($this->storage, $file, $fileId, $storageId);
308
+                $this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
309
+                $this->eventDispatcher->dispatchTyped($event);
310
+                return $fileId;
311
+            }
312
+        } catch (Exception $e) {
313
+            if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
314
+                // entry exists already
315
+                if ($this->connection->inTransaction()) {
316
+                    $this->connection->commit();
317
+                    $this->connection->beginTransaction();
318
+                }
319
+            } else {
320
+                throw $e;
321
+            }
322
+        }
323
+
324
+        // The file was created in the meantime
325
+        if (($id = $this->getId($file)) > -1) {
326
+            $this->update($id, $data);
327
+            return $id;
328
+        } else {
329
+            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.');
330
+        }
331
+    }
332
+
333
+    /**
334
+     * update the metadata of an existing file or folder in the cache
335
+     *
336
+     * @param int $id the fileid of the existing file or folder
337
+     * @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
338
+     */
339
+    public function update($id, array $data) {
340
+        if (isset($data['path'])) {
341
+            // normalize path
342
+            $data['path'] = $this->normalize($data['path']);
343
+        }
344
+
345
+        if (isset($data['name'])) {
346
+            // normalize path
347
+            $data['name'] = $this->normalize($data['name']);
348
+        }
349
+
350
+        [$values, $extensionValues] = $this->normalizeData($data);
351
+
352
+        if (count($values)) {
353
+            $query = $this->getQueryBuilder();
354
+
355
+            $query->update('filecache')
356
+                ->whereFileId($id)
357
+                ->whereStorageId($this->getNumericStorageId())
358
+                ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
359
+                    return $query->expr()->orX(
360
+                        $query->expr()->neq($key, $query->createNamedParameter($value)),
361
+                        $query->expr()->isNull($key)
362
+                    );
363
+                }, array_keys($values), array_values($values))));
364
+
365
+            foreach ($values as $key => $value) {
366
+                $query->set($key, $query->createNamedParameter($value));
367
+            }
368
+
369
+            $query->executeStatement();
370
+        }
371
+
372
+        if (count($extensionValues)) {
373
+            try {
374
+                $query = $this->getQueryBuilder();
375
+                $query->insert('filecache_extended');
376
+                $query->hintShardKey('storage', $this->getNumericStorageId());
377
+
378
+                $query->setValue('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT));
379
+                foreach ($extensionValues as $column => $value) {
380
+                    $query->setValue($column, $query->createNamedParameter($value));
381
+                }
382
+
383
+                $query->executeStatement();
384
+            } catch (Exception $e) {
385
+                if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
386
+                    throw $e;
387
+                }
388
+                $query = $this->getQueryBuilder();
389
+                $query->update('filecache_extended')
390
+                    ->whereFileId($id)
391
+                    ->hintShardKey('storage', $this->getNumericStorageId())
392
+                    ->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
393
+                        return $query->expr()->orX(
394
+                            $query->expr()->neq($key, $query->createNamedParameter($value)),
395
+                            $query->expr()->isNull($key)
396
+                        );
397
+                    }, array_keys($extensionValues), array_values($extensionValues))));
398
+
399
+                foreach ($extensionValues as $key => $value) {
400
+                    $query->set($key, $query->createNamedParameter($value));
401
+                }
402
+
403
+                $query->executeStatement();
404
+            }
405
+        }
406
+
407
+        $path = $this->getPathById($id);
408
+        // path can still be null if the file doesn't exist
409
+        if ($path !== null) {
410
+            $event = new CacheEntryUpdatedEvent($this->storage, $path, $id, $this->getNumericStorageId());
411
+            $this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
412
+            $this->eventDispatcher->dispatchTyped($event);
413
+        }
414
+    }
415
+
416
+    /**
417
+     * extract query parts and params array from data array
418
+     *
419
+     * @param array $data
420
+     * @return array
421
+     */
422
+    protected function normalizeData(array $data): array {
423
+        $fields = [
424
+            'path', 'parent', 'name', 'mimetype', 'size', 'mtime', 'storage_mtime', 'encrypted',
425
+            'etag', 'permissions', 'checksum', 'storage', 'unencrypted_size'];
426
+        $extensionFields = ['metadata_etag', 'creation_time', 'upload_time'];
427
+
428
+        $doNotCopyStorageMTime = false;
429
+        if (array_key_exists('mtime', $data) && $data['mtime'] === null) {
430
+            // this horrific magic tells it to not copy storage_mtime to mtime
431
+            unset($data['mtime']);
432
+            $doNotCopyStorageMTime = true;
433
+        }
434
+
435
+        $params = [];
436
+        $extensionParams = [];
437
+        foreach ($data as $name => $value) {
438
+            if (in_array($name, $fields)) {
439
+                if ($name === 'path') {
440
+                    $params['path_hash'] = md5($value);
441
+                } elseif ($name === 'mimetype') {
442
+                    $params['mimepart'] = $this->mimetypeLoader->getId(substr($value, 0, strpos($value, '/')));
443
+                    $value = $this->mimetypeLoader->getId($value);
444
+                } elseif ($name === 'storage_mtime') {
445
+                    if (!$doNotCopyStorageMTime && !isset($data['mtime'])) {
446
+                        $params['mtime'] = $value;
447
+                    }
448
+                } elseif ($name === 'encrypted') {
449
+                    if (isset($data['encryptedVersion'])) {
450
+                        $value = $data['encryptedVersion'];
451
+                    } else {
452
+                        // Boolean to integer conversion
453
+                        $value = $value ? 1 : 0;
454
+                    }
455
+                }
456
+                $params[$name] = $value;
457
+            }
458
+            if (in_array($name, $extensionFields)) {
459
+                $extensionParams[$name] = $value;
460
+            }
461
+        }
462
+        return [$params, array_filter($extensionParams)];
463
+    }
464
+
465
+    /**
466
+     * get the file id for a file
467
+     *
468
+     * 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
469
+     *
470
+     * File ids are easiest way for apps to store references to a file since unlike paths they are not affected by renames or sharing
471
+     *
472
+     * @param string $file
473
+     * @return int
474
+     */
475
+    public function getId($file) {
476
+        // normalize file
477
+        $file = $this->normalize($file);
478
+
479
+        $query = $this->getQueryBuilder();
480
+        $query->select('fileid')
481
+            ->from('filecache')
482
+            ->whereStorageId($this->getNumericStorageId())
483
+            ->wherePath($file);
484
+
485
+        $result = $query->executeQuery();
486
+        $id = $result->fetchOne();
487
+        $result->closeCursor();
488
+
489
+        return $id === false ? -1 : (int)$id;
490
+    }
491
+
492
+    /**
493
+     * get the id of the parent folder of a file
494
+     *
495
+     * @param string $file
496
+     * @return int
497
+     */
498
+    public function getParentId($file) {
499
+        if ($file === '') {
500
+            return -1;
501
+        } else {
502
+            $parent = $this->getParentPath($file);
503
+            return (int)$this->getId($parent);
504
+        }
505
+    }
506
+
507
+    private function getParentPath($path) {
508
+        $parent = dirname($path);
509
+        if ($parent === '.') {
510
+            $parent = '';
511
+        }
512
+        return $parent;
513
+    }
514
+
515
+    /**
516
+     * check if a file is available in the cache
517
+     *
518
+     * @param string $file
519
+     * @return bool
520
+     */
521
+    public function inCache($file) {
522
+        return $this->getId($file) != -1;
523
+    }
524
+
525
+    /**
526
+     * remove a file or folder from the cache
527
+     *
528
+     * when removing a folder from the cache all files and folders inside the folder will be removed as well
529
+     *
530
+     * @param string $file
531
+     */
532
+    public function remove($file) {
533
+        $entry = $this->get($file);
534
+
535
+        if ($entry instanceof ICacheEntry) {
536
+            $query = $this->getQueryBuilder();
537
+            $query->delete('filecache')
538
+                ->whereStorageId($this->getNumericStorageId())
539
+                ->whereFileId($entry->getId());
540
+            $query->executeStatement();
541
+
542
+            $query = $this->getQueryBuilder();
543
+            $query->delete('filecache_extended')
544
+                ->whereFileId($entry->getId())
545
+                ->hintShardKey('storage', $this->getNumericStorageId());
546
+            $query->executeStatement();
547
+
548
+            if ($entry->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
549
+                $this->removeChildren($entry);
550
+            }
551
+
552
+            $this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $entry->getPath(), $entry->getId(), $this->getNumericStorageId()));
553
+        }
554
+    }
555
+
556
+    /**
557
+     * Remove all children of a folder
558
+     *
559
+     * @param ICacheEntry $entry the cache entry of the folder to remove the children of
560
+     * @throws \OC\DatabaseException
561
+     */
562
+    private function removeChildren(ICacheEntry $entry) {
563
+        $parentIds = [$entry->getId()];
564
+        $queue = [$entry->getId()];
565
+        $deletedIds = [];
566
+        $deletedPaths = [];
567
+
568
+        // we walk depth first through the file tree, removing all filecache_extended attributes while we walk
569
+        // and collecting all folder ids to later use to delete the filecache entries
570
+        while ($entryId = array_pop($queue)) {
571
+            $children = $this->getFolderContentsById($entryId);
572
+            $childIds = array_map(function (ICacheEntry $cacheEntry) {
573
+                return $cacheEntry->getId();
574
+            }, $children);
575
+            $childPaths = array_map(function (ICacheEntry $cacheEntry) {
576
+                return $cacheEntry->getPath();
577
+            }, $children);
578
+
579
+            foreach ($childIds as $childId) {
580
+                $deletedIds[] = $childId;
581
+            }
582
+
583
+            foreach ($childPaths as $childPath) {
584
+                $deletedPaths[] = $childPath;
585
+            }
586
+
587
+            $query = $this->getQueryBuilder();
588
+            $query->delete('filecache_extended')
589
+                ->where($query->expr()->in('fileid', $query->createParameter('childIds')))
590
+                ->hintShardKey('storage', $this->getNumericStorageId());
591
+
592
+            foreach (array_chunk($childIds, 1000) as $childIdChunk) {
593
+                $query->setParameter('childIds', $childIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
594
+                $query->executeStatement();
595
+            }
596
+
597
+            /** @var ICacheEntry[] $childFolders */
598
+            $childFolders = [];
599
+            foreach ($children as $child) {
600
+                if ($child->getMimeType() == FileInfo::MIMETYPE_FOLDER) {
601
+                    $childFolders[] = $child;
602
+                }
603
+            }
604
+            foreach ($childFolders as $folder) {
605
+                $parentIds[] = $folder->getId();
606
+                $queue[] = $folder->getId();
607
+            }
608
+        }
609
+
610
+        $query = $this->getQueryBuilder();
611
+        $query->delete('filecache')
612
+            ->whereStorageId($this->getNumericStorageId())
613
+            ->whereParentInParameter('parentIds');
614
+
615
+        // Sorting before chunking allows the db to find the entries close to each
616
+        // other in the index
617
+        sort($parentIds, SORT_NUMERIC);
618
+        foreach (array_chunk($parentIds, 1000) as $parentIdChunk) {
619
+            $query->setParameter('parentIds', $parentIdChunk, IQueryBuilder::PARAM_INT_ARRAY);
620
+            $query->executeStatement();
621
+        }
622
+
623
+        foreach (array_combine($deletedIds, $deletedPaths) as $fileId => $filePath) {
624
+            $cacheEntryRemovedEvent = new CacheEntryRemovedEvent(
625
+                $this->storage,
626
+                $filePath,
627
+                $fileId,
628
+                $this->getNumericStorageId()
629
+            );
630
+            $this->eventDispatcher->dispatchTyped($cacheEntryRemovedEvent);
631
+        }
632
+    }
633
+
634
+    /**
635
+     * Move a file or folder in the cache
636
+     *
637
+     * @param string $source
638
+     * @param string $target
639
+     */
640
+    public function move($source, $target) {
641
+        $this->moveFromCache($this, $source, $target);
642
+    }
643
+
644
+    /**
645
+     * Get the storage id and path needed for a move
646
+     *
647
+     * @param string $path
648
+     * @return array [$storageId, $internalPath]
649
+     */
650
+    protected function getMoveInfo($path) {
651
+        return [$this->getNumericStorageId(), $path];
652
+    }
653
+
654
+    protected function hasEncryptionWrapper(): bool {
655
+        return $this->storage->instanceOfStorage(Encryption::class);
656
+    }
657
+
658
+    protected function shouldEncrypt(string $targetPath): bool {
659
+        if (!$this->storage->instanceOfStorage(Encryption::class)) {
660
+            return false;
661
+        }
662
+        return $this->storage->shouldEncrypt($targetPath);
663
+    }
664
+
665
+    /**
666
+     * Move a file or folder in the cache
667
+     *
668
+     * @param ICache $sourceCache
669
+     * @param string $sourcePath
670
+     * @param string $targetPath
671
+     * @throws \OC\DatabaseException
672
+     * @throws \Exception if the given storages have an invalid id
673
+     */
674
+    public function moveFromCache(ICache $sourceCache, $sourcePath, $targetPath) {
675
+        if ($sourceCache instanceof Cache) {
676
+            // normalize source and target
677
+            $sourcePath = $this->normalize($sourcePath);
678
+            $targetPath = $this->normalize($targetPath);
679
+
680
+            $sourceData = $sourceCache->get($sourcePath);
681
+            if (!$sourceData) {
682
+                throw new \Exception('Source path not found in cache: ' . $sourcePath);
683
+            }
684
+
685
+            $shardDefinition = $this->connection->getShardDefinition('filecache');
686
+            if (
687
+                $shardDefinition
688
+                && $shardDefinition->getShardForKey($sourceCache->getNumericStorageId()) !== $shardDefinition->getShardForKey($this->getNumericStorageId())
689
+            ) {
690
+                $this->moveFromStorageSharded($shardDefinition, $sourceCache, $sourceData, $targetPath);
691
+                return;
692
+            }
693
+
694
+            $sourceId = $sourceData['fileid'];
695
+            $newParentId = $this->getParentId($targetPath);
696
+
697
+            [$sourceStorageId, $sourcePath] = $sourceCache->getMoveInfo($sourcePath);
698
+            [$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
699
+
700
+            if (is_null($sourceStorageId) || $sourceStorageId === false) {
701
+                throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
702
+            }
703
+            if (is_null($targetStorageId) || $targetStorageId === false) {
704
+                throw new \Exception('Invalid target storage id: ' . $targetStorageId);
705
+            }
706
+
707
+            if ($sourceData['mimetype'] === 'httpd/unix-directory') {
708
+                //update all child entries
709
+                $sourceLength = mb_strlen($sourcePath);
710
+
711
+                $childIds = $this->getChildIds($sourceStorageId, $sourcePath);
712
+
713
+                $childChunks = array_chunk($childIds, 1000);
714
+
715
+                $query = $this->getQueryBuilder();
716
+
717
+                $fun = $query->func();
718
+                $newPathFunction = $fun->concat(
719
+                    $query->createNamedParameter($targetPath),
720
+                    $fun->substring('path', $query->createNamedParameter($sourceLength + 1, IQueryBuilder::PARAM_INT))// +1 for the leading slash
721
+                );
722
+                $query->update('filecache')
723
+                    ->set('path_hash', $fun->md5($newPathFunction))
724
+                    ->set('path', $newPathFunction)
725
+                    ->whereStorageId($sourceStorageId)
726
+                    ->andWhere($query->expr()->in('fileid', $query->createParameter('files')));
727
+
728
+                if ($sourceStorageId !== $targetStorageId) {
729
+                    $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
730
+                }
731
+
732
+                // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
733
+                if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
734
+                    $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
735
+                }
736
+
737
+                // Retry transaction in case of RetryableException like deadlocks.
738
+                // Retry up to 4 times because we should receive up to 4 concurrent requests from the frontend
739
+                $retryLimit = 4;
740
+                for ($i = 1; $i <= $retryLimit; $i++) {
741
+                    try {
742
+                        $this->connection->beginTransaction();
743
+                        foreach ($childChunks as $chunk) {
744
+                            $query->setParameter('files', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
745
+                            $query->executeStatement();
746
+                        }
747
+                        break;
748
+                    } catch (\OC\DatabaseException $e) {
749
+                        $this->connection->rollBack();
750
+                        throw $e;
751
+                    } catch (DbalException $e) {
752
+                        $this->connection->rollBack();
753
+
754
+                        if (!$e->isRetryable()) {
755
+                            throw $e;
756
+                        }
757
+
758
+                        // Simply throw if we already retried 4 times.
759
+                        if ($i === $retryLimit) {
760
+                            throw $e;
761
+                        }
762
+
763
+                        // Sleep a bit to give some time to the other transaction to finish.
764
+                        usleep(100 * 1000 * $i);
765
+                    }
766
+                }
767
+            } else {
768
+                $this->connection->beginTransaction();
769
+            }
770
+
771
+            $query = $this->getQueryBuilder();
772
+            $query->update('filecache')
773
+                ->set('path', $query->createNamedParameter($targetPath))
774
+                ->set('path_hash', $query->createNamedParameter(md5($targetPath)))
775
+                ->set('name', $query->createNamedParameter(basename($targetPath)))
776
+                ->set('parent', $query->createNamedParameter($newParentId, IQueryBuilder::PARAM_INT))
777
+                ->whereStorageId($sourceStorageId)
778
+                ->whereFileId($sourceId);
779
+
780
+            if ($sourceStorageId !== $targetStorageId) {
781
+                $query->set('storage', $query->createNamedParameter($targetStorageId), IQueryBuilder::PARAM_INT);
782
+            }
783
+
784
+            // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
785
+            if ($sourceCache->hasEncryptionWrapper() && !$this->hasEncryptionWrapper()) {
786
+                $query->set('encrypted', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT));
787
+            }
788
+
789
+            $query->executeStatement();
790
+
791
+            $this->connection->commit();
792
+
793
+            if ($sourceCache->getNumericStorageId() !== $this->getNumericStorageId()) {
794
+                $this->eventDispatcher->dispatchTyped(new CacheEntryRemovedEvent($this->storage, $sourcePath, $sourceId, $sourceCache->getNumericStorageId()));
795
+                $event = new CacheEntryInsertedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
796
+                $this->eventDispatcher->dispatch(CacheInsertEvent::class, $event);
797
+                $this->eventDispatcher->dispatchTyped($event);
798
+            } else {
799
+                $event = new CacheEntryUpdatedEvent($this->storage, $targetPath, $sourceId, $this->getNumericStorageId());
800
+                $this->eventDispatcher->dispatch(CacheUpdateEvent::class, $event);
801
+                $this->eventDispatcher->dispatchTyped($event);
802
+            }
803
+        } else {
804
+            $this->moveFromCacheFallback($sourceCache, $sourcePath, $targetPath);
805
+        }
806
+    }
807
+
808
+    private function getChildIds(int $storageId, string $path): array {
809
+        $query = $this->connection->getQueryBuilder();
810
+        $query->select('fileid')
811
+            ->from('filecache')
812
+            ->where($query->expr()->eq('storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
813
+            ->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($path) . '/%')));
814
+        return $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
815
+    }
816
+
817
+    /**
818
+     * remove all entries for files that are stored on the storage from the cache
819
+     */
820
+    public function clear() {
821
+        $query = $this->getQueryBuilder();
822
+        $query->delete('filecache')
823
+            ->whereStorageId($this->getNumericStorageId());
824
+        $query->executeStatement();
825
+
826
+        $query = $this->connection->getQueryBuilder();
827
+        $query->delete('storages')
828
+            ->where($query->expr()->eq('id', $query->createNamedParameter($this->storageId)));
829
+        $query->executeStatement();
830
+    }
831
+
832
+    /**
833
+     * Get the scan status of a file
834
+     *
835
+     * - Cache::NOT_FOUND: File is not in the cache
836
+     * - Cache::PARTIAL: File is not stored in the cache but some incomplete data is known
837
+     * - Cache::SHALLOW: The folder and it's direct children are in the cache but not all sub folders are fully scanned
838
+     * - Cache::COMPLETE: The file or folder, with all it's children) are fully scanned
839
+     *
840
+     * @param string $file
841
+     *
842
+     * @return int Cache::NOT_FOUND, Cache::PARTIAL, Cache::SHALLOW or Cache::COMPLETE
843
+     */
844
+    public function getStatus($file) {
845
+        // normalize file
846
+        $file = $this->normalize($file);
847
+
848
+        $query = $this->getQueryBuilder();
849
+        $query->select('size')
850
+            ->from('filecache')
851
+            ->whereStorageId($this->getNumericStorageId())
852
+            ->wherePath($file);
853
+
854
+        $result = $query->executeQuery();
855
+        $size = $result->fetchOne();
856
+        $result->closeCursor();
857
+
858
+        if ($size !== false) {
859
+            if ((int)$size === -1) {
860
+                return self::SHALLOW;
861
+            } else {
862
+                return self::COMPLETE;
863
+            }
864
+        } else {
865
+            if (isset($this->partial[$file])) {
866
+                return self::PARTIAL;
867
+            } else {
868
+                return self::NOT_FOUND;
869
+            }
870
+        }
871
+    }
872
+
873
+    /**
874
+     * search for files matching $pattern
875
+     *
876
+     * @param string $pattern the search pattern using SQL search syntax (e.g. '%searchstring%')
877
+     * @return ICacheEntry[] an array of cache entries where the name matches the search pattern
878
+     */
879
+    public function search($pattern) {
880
+        $operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'name', $pattern);
881
+        return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
882
+    }
883
+
884
+    /**
885
+     * search for files by mimetype
886
+     *
887
+     * @param string $mimetype either a full mimetype to search ('text/plain') or only the first part of a mimetype ('image')
888
+     *                         where it will search for all mimetypes in the group ('image/*')
889
+     * @return ICacheEntry[] an array of cache entries where the mimetype matches the search
890
+     */
891
+    public function searchByMime($mimetype) {
892
+        if (!str_contains($mimetype, '/')) {
893
+            $operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%');
894
+        } else {
895
+            $operator = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype);
896
+        }
897
+        return $this->searchQuery(new SearchQuery($operator, 0, 0, [], null));
898
+    }
899
+
900
+    public function searchQuery(ISearchQuery $query) {
901
+        return current($this->querySearchHelper->searchInCaches($query, [$this]));
902
+    }
903
+
904
+    /**
905
+     * Re-calculate the folder size and the size of all parent folders
906
+     *
907
+     * @param array|ICacheEntry|null $data (optional) meta data of the folder
908
+     */
909
+    public function correctFolderSize(string $path, $data = null, bool $isBackgroundScan = false): void {
910
+        $this->calculateFolderSize($path, $data);
911
+
912
+        if ($path !== '') {
913
+            $parent = dirname($path);
914
+            if ($parent === '.' || $parent === '/') {
915
+                $parent = '';
916
+            }
917
+
918
+            if ($isBackgroundScan) {
919
+                $parentData = $this->get($parent);
920
+                if ($parentData !== false
921
+                    && $parentData['size'] !== -1
922
+                    && $this->getIncompleteChildrenCount($parentData['fileid']) === 0
923
+                ) {
924
+                    $this->correctFolderSize($parent, $parentData, $isBackgroundScan);
925
+                }
926
+            } else {
927
+                $this->correctFolderSize($parent);
928
+            }
929
+        }
930
+    }
931
+
932
+    /**
933
+     * get the incomplete count that shares parent $folder
934
+     *
935
+     * @param int $fileId the file id of the folder
936
+     * @return int
937
+     */
938
+    public function getIncompleteChildrenCount($fileId) {
939
+        if ($fileId > -1) {
940
+            $query = $this->getQueryBuilder();
941
+            $query->select($query->func()->count())
942
+                ->from('filecache')
943
+                ->whereParent($fileId)
944
+                ->whereStorageId($this->getNumericStorageId())
945
+                ->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)));
946
+
947
+            $result = $query->executeQuery();
948
+            $size = (int)$result->fetchOne();
949
+            $result->closeCursor();
950
+
951
+            return $size;
952
+        }
953
+        return -1;
954
+    }
955
+
956
+    /**
957
+     * calculate the size of a folder and set it in the cache
958
+     *
959
+     * @param string $path
960
+     * @param array|null|ICacheEntry $entry (optional) meta data of the folder
961
+     * @return int|float
962
+     */
963
+    public function calculateFolderSize($path, $entry = null) {
964
+        return $this->calculateFolderSizeInner($path, $entry);
965
+    }
966
+
967
+
968
+    /**
969
+     * inner function because we can't add new params to the public function without breaking any child classes
970
+     *
971
+     * @param string $path
972
+     * @param array|null|ICacheEntry $entry (optional) meta data of the folder
973
+     * @param bool $ignoreUnknown don't mark the folder size as unknown if any of it's children are unknown
974
+     * @return int|float
975
+     */
976
+    protected function calculateFolderSizeInner(string $path, $entry = null, bool $ignoreUnknown = false) {
977
+        $totalSize = 0;
978
+        if (is_null($entry) || !isset($entry['fileid'])) {
979
+            $entry = $this->get($path);
980
+        }
981
+        if (isset($entry['mimetype']) && $entry['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
982
+            $id = $entry['fileid'];
983
+
984
+            $query = $this->getQueryBuilder();
985
+            $query->select('size', 'unencrypted_size')
986
+                ->from('filecache')
987
+                ->whereStorageId($this->getNumericStorageId())
988
+                ->whereParent($id);
989
+            if ($ignoreUnknown) {
990
+                $query->andWhere($query->expr()->gte('size', $query->createNamedParameter(0)));
991
+            }
992
+
993
+            $result = $query->executeQuery();
994
+            $rows = $result->fetchAll();
995
+            $result->closeCursor();
996
+
997
+            if ($rows) {
998
+                $sizes = array_map(function (array $row) {
999
+                    return Util::numericToNumber($row['size']);
1000
+                }, $rows);
1001
+                $unencryptedOnlySizes = array_map(function (array $row) {
1002
+                    return Util::numericToNumber($row['unencrypted_size']);
1003
+                }, $rows);
1004
+                $unencryptedSizes = array_map(function (array $row) {
1005
+                    return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
1006
+                }, $rows);
1007
+
1008
+                $sum = array_sum($sizes);
1009
+                $min = min($sizes);
1010
+
1011
+                $unencryptedSum = array_sum($unencryptedSizes);
1012
+                $unencryptedMin = min($unencryptedSizes);
1013
+                $unencryptedMax = max($unencryptedOnlySizes);
1014
+
1015
+                $sum = 0 + $sum;
1016
+                $min = 0 + $min;
1017
+                if ($min === -1) {
1018
+                    $totalSize = $min;
1019
+                } else {
1020
+                    $totalSize = $sum;
1021
+                }
1022
+                if ($unencryptedMin === -1 || $min === -1) {
1023
+                    $unencryptedTotal = $unencryptedMin;
1024
+                } else {
1025
+                    $unencryptedTotal = $unencryptedSum;
1026
+                }
1027
+            } else {
1028
+                $totalSize = 0;
1029
+                $unencryptedTotal = 0;
1030
+                $unencryptedMax = 0;
1031
+            }
1032
+
1033
+            // only set unencrypted size for a folder if any child entries have it set, or the folder is empty
1034
+            $shouldWriteUnEncryptedSize = $unencryptedMax > 0 || $totalSize === 0 || ($entry['unencrypted_size'] ?? 0) > 0;
1035
+            if ($entry['size'] !== $totalSize || (($entry['unencrypted_size'] ?? 0) !== $unencryptedTotal && $shouldWriteUnEncryptedSize)) {
1036
+                if ($shouldWriteUnEncryptedSize) {
1037
+                    // if all children have an unencrypted size of 0, just set the folder unencrypted size to 0 instead of summing the sizes
1038
+                    if ($unencryptedMax === 0) {
1039
+                        $unencryptedTotal = 0;
1040
+                    }
1041
+
1042
+                    $this->update($id, [
1043
+                        'size' => $totalSize,
1044
+                        'unencrypted_size' => $unencryptedTotal,
1045
+                    ]);
1046
+                } else {
1047
+                    $this->update($id, [
1048
+                        'size' => $totalSize,
1049
+                    ]);
1050
+                }
1051
+            }
1052
+        }
1053
+        return $totalSize;
1054
+    }
1055
+
1056
+    /**
1057
+     * get all file ids on the files on the storage
1058
+     *
1059
+     * @return int[]
1060
+     */
1061
+    public function getAll() {
1062
+        $query = $this->getQueryBuilder();
1063
+        $query->select('fileid')
1064
+            ->from('filecache')
1065
+            ->whereStorageId($this->getNumericStorageId());
1066
+
1067
+        $result = $query->executeQuery();
1068
+        $files = $result->fetchAll(\PDO::FETCH_COLUMN);
1069
+        $result->closeCursor();
1070
+
1071
+        return array_map(function ($id) {
1072
+            return (int)$id;
1073
+        }, $files);
1074
+    }
1075
+
1076
+    /**
1077
+     * find a folder in the cache which has not been fully scanned
1078
+     *
1079
+     * If multiple incomplete folders are in the cache, the one with the highest id will be returned,
1080
+     * use the one with the highest id gives the best result with the background scanner, since that is most
1081
+     * likely the folder where we stopped scanning previously
1082
+     *
1083
+     * @return string|false the path of the folder or false when no folder matched
1084
+     */
1085
+    public function getIncomplete() {
1086
+        $query = $this->getQueryBuilder();
1087
+        $query->select('path')
1088
+            ->from('filecache')
1089
+            ->whereStorageId($this->getNumericStorageId())
1090
+            ->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)))
1091
+            ->orderBy('fileid', 'DESC')
1092
+            ->setMaxResults(1);
1093
+
1094
+        $result = $query->executeQuery();
1095
+        $path = $result->fetchOne();
1096
+        $result->closeCursor();
1097
+
1098
+        return $path === false ? false : (string)$path;
1099
+    }
1100
+
1101
+    /**
1102
+     * get the path of a file on this storage by it's file id
1103
+     *
1104
+     * @param int $id the file id of the file or folder to search
1105
+     * @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
1106
+     */
1107
+    public function getPathById($id) {
1108
+        $query = $this->getQueryBuilder();
1109
+        $query->select('path')
1110
+            ->from('filecache')
1111
+            ->whereStorageId($this->getNumericStorageId())
1112
+            ->whereFileId($id);
1113
+
1114
+        $result = $query->executeQuery();
1115
+        $path = $result->fetchOne();
1116
+        $result->closeCursor();
1117
+
1118
+        if ($path === false) {
1119
+            return null;
1120
+        }
1121
+
1122
+        return (string)$path;
1123
+    }
1124
+
1125
+    /**
1126
+     * get the storage id of the storage for a file and the internal path of the file
1127
+     * unlike getPathById this does not limit the search to files on this storage and
1128
+     * instead does a global search in the cache table
1129
+     *
1130
+     * @param int $id
1131
+     * @return array first element holding the storage id, second the path
1132
+     * @deprecated 17.0.0 use getPathById() instead
1133
+     */
1134
+    public static function getById($id) {
1135
+        $query = \OC::$server->getDatabaseConnection()->getQueryBuilder();
1136
+        $query->select('path', 'storage')
1137
+            ->from('filecache')
1138
+            ->where($query->expr()->eq('fileid', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
1139
+
1140
+        $result = $query->executeQuery();
1141
+        $row = $result->fetch();
1142
+        $result->closeCursor();
1143
+
1144
+        if ($row) {
1145
+            $numericId = $row['storage'];
1146
+            $path = $row['path'];
1147
+        } else {
1148
+            return null;
1149
+        }
1150
+
1151
+        if ($id = Storage::getStorageId($numericId)) {
1152
+            return [$id, $path];
1153
+        } else {
1154
+            return null;
1155
+        }
1156
+    }
1157
+
1158
+    /**
1159
+     * normalize the given path
1160
+     *
1161
+     * @param string $path
1162
+     * @return string
1163
+     */
1164
+    public function normalize($path) {
1165
+        return trim(\OC_Util::normalizeUnicode($path), '/');
1166
+    }
1167
+
1168
+    /**
1169
+     * Copy a file or folder in the cache
1170
+     *
1171
+     * @param ICache $sourceCache
1172
+     * @param ICacheEntry $sourceEntry
1173
+     * @param string $targetPath
1174
+     * @return int fileId of copied entry
1175
+     */
1176
+    public function copyFromCache(ICache $sourceCache, ICacheEntry $sourceEntry, string $targetPath): int {
1177
+        if ($sourceEntry->getId() < 0) {
1178
+            throw new \RuntimeException('Invalid source cache entry on copyFromCache');
1179
+        }
1180
+        $data = $this->cacheEntryToArray($sourceEntry);
1181
+
1182
+        // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1183
+        if ($sourceCache instanceof Cache
1184
+            && $sourceCache->hasEncryptionWrapper()
1185
+            && !$this->shouldEncrypt($targetPath)) {
1186
+            $data['encrypted'] = 0;
1187
+        }
1188
+
1189
+        $fileId = $this->put($targetPath, $data);
1190
+        if ($fileId <= 0) {
1191
+            throw new \RuntimeException('Failed to copy to ' . $targetPath . ' from cache with source data ' . json_encode($data) . ' ');
1192
+        }
1193
+        if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1194
+            $folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1195
+            foreach ($folderContent as $subEntry) {
1196
+                $subTargetPath = $targetPath . '/' . $subEntry->getName();
1197
+                $this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1198
+            }
1199
+        }
1200
+        return $fileId;
1201
+    }
1202
+
1203
+    private function cacheEntryToArray(ICacheEntry $entry): array {
1204
+        $data = [
1205
+            'size' => $entry->getSize(),
1206
+            'mtime' => $entry->getMTime(),
1207
+            'storage_mtime' => $entry->getStorageMTime(),
1208
+            'mimetype' => $entry->getMimeType(),
1209
+            'mimepart' => $entry->getMimePart(),
1210
+            'etag' => $entry->getEtag(),
1211
+            'permissions' => $entry->getPermissions(),
1212
+            'encrypted' => $entry->isEncrypted(),
1213
+            'creation_time' => $entry->getCreationTime(),
1214
+            'upload_time' => $entry->getUploadTime(),
1215
+            'metadata_etag' => $entry->getMetadataEtag(),
1216
+        ];
1217
+        if ($entry instanceof CacheEntry && isset($entry['scan_permissions'])) {
1218
+            $data['permissions'] = $entry['scan_permissions'];
1219
+        }
1220
+        return $data;
1221
+    }
1222
+
1223
+    public function getQueryFilterForStorage(): ISearchOperator {
1224
+        return new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'storage', $this->getNumericStorageId());
1225
+    }
1226
+
1227
+    public function getCacheEntryFromSearchResult(ICacheEntry $rawEntry): ?ICacheEntry {
1228
+        if ($rawEntry->getStorageId() === $this->getNumericStorageId()) {
1229
+            return $rawEntry;
1230
+        } else {
1231
+            return null;
1232
+        }
1233
+    }
1234
+
1235
+    private function moveFromStorageSharded(ShardDefinition $shardDefinition, ICache $sourceCache, ICacheEntry $sourceEntry, $targetPath): void {
1236
+        $sourcePath = $sourceEntry->getPath();
1237
+        while ($sourceCache instanceof CacheWrapper) {
1238
+            if ($sourceCache instanceof CacheJail) {
1239
+                $sourcePath = $sourceCache->getSourcePath($sourcePath);
1240
+            }
1241
+            $sourceCache = $sourceCache->getCache();
1242
+        }
1243
+
1244
+        if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1245
+            $fileIds = $this->getChildIds($sourceCache->getNumericStorageId(), $sourcePath);
1246
+        } else {
1247
+            $fileIds = [];
1248
+        }
1249
+        $fileIds[] = $sourceEntry->getId();
1250
+
1251
+        $helper = $this->connection->getCrossShardMoveHelper();
1252
+
1253
+        $sourceConnection = $helper->getConnection($shardDefinition, $sourceCache->getNumericStorageId());
1254
+        $targetConnection = $helper->getConnection($shardDefinition, $this->getNumericStorageId());
1255
+
1256
+        $cacheItems = $helper->loadItems($sourceConnection, 'filecache', 'fileid', $fileIds);
1257
+        $extendedItems = $helper->loadItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
1258
+        $metadataItems = $helper->loadItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
1259
+
1260
+        // when moving from an encrypted storage to a non-encrypted storage remove the `encrypted` mark
1261
+        $removeEncryptedFlag = ($sourceCache instanceof Cache && $sourceCache->hasEncryptionWrapper()) && !$this->hasEncryptionWrapper();
1262
+
1263
+        $sourcePathLength = strlen($sourcePath);
1264
+        foreach ($cacheItems as &$cacheItem) {
1265
+            if ($cacheItem['path'] === $sourcePath) {
1266
+                $cacheItem['path'] = $targetPath;
1267
+                $cacheItem['parent'] = $this->getParentId($targetPath);
1268
+                $cacheItem['name'] = basename($cacheItem['path']);
1269
+            } else {
1270
+                $cacheItem['path'] = $targetPath . '/' . substr($cacheItem['path'], $sourcePathLength + 1); // +1 for the leading slash
1271
+            }
1272
+            $cacheItem['path_hash'] = md5($cacheItem['path']);
1273
+            $cacheItem['storage'] = $this->getNumericStorageId();
1274
+            if ($removeEncryptedFlag) {
1275
+                $cacheItem['encrypted'] = 0;
1276
+            }
1277
+        }
1278
+
1279
+        $targetConnection->beginTransaction();
1280
+
1281
+        try {
1282
+            $helper->saveItems($targetConnection, 'filecache', $cacheItems);
1283
+            $helper->saveItems($targetConnection, 'filecache_extended', $extendedItems);
1284
+            $helper->saveItems($targetConnection, 'files_metadata', $metadataItems);
1285
+        } catch (\Exception $e) {
1286
+            $targetConnection->rollback();
1287
+            throw $e;
1288
+        }
1289
+
1290
+        $sourceConnection->beginTransaction();
1291
+
1292
+        try {
1293
+            $helper->deleteItems($sourceConnection, 'filecache', 'fileid', $fileIds);
1294
+            $helper->deleteItems($sourceConnection, 'filecache_extended', 'fileid', $fileIds);
1295
+            $helper->deleteItems($sourceConnection, 'files_metadata', 'file_id', $fileIds);
1296
+        } catch (\Exception $e) {
1297
+            $targetConnection->rollback();
1298
+            $sourceConnection->rollBack();
1299
+            throw $e;
1300
+        }
1301
+
1302
+        try {
1303
+            $sourceConnection->commit();
1304
+        } catch (\Exception $e) {
1305
+            $targetConnection->rollback();
1306
+            throw $e;
1307
+        }
1308
+        $targetConnection->commit();
1309
+    }
1310 1310
 }
Please login to merge, or discard this patch.
Spacing   +37 added lines, -37 removed lines patch added patch discarded remove patch
@@ -155,18 +155,18 @@  discard block
 block discarded – undo
155 155
 	 */
156 156
 	public static function cacheEntryFromData($data, IMimeTypeLoader $mimetypeLoader) {
157 157
 		//fix types
158
-		$data['name'] = (string)$data['name'];
159
-		$data['path'] = (string)$data['path'];
160
-		$data['fileid'] = (int)$data['fileid'];
161
-		$data['parent'] = (int)$data['parent'];
158
+		$data['name'] = (string) $data['name'];
159
+		$data['path'] = (string) $data['path'];
160
+		$data['fileid'] = (int) $data['fileid'];
161
+		$data['parent'] = (int) $data['parent'];
162 162
 		$data['size'] = Util::numericToNumber($data['size']);
163 163
 		$data['unencrypted_size'] = Util::numericToNumber($data['unencrypted_size'] ?? 0);
164
-		$data['mtime'] = (int)$data['mtime'];
165
-		$data['storage_mtime'] = (int)$data['storage_mtime'];
166
-		$data['encryptedVersion'] = (int)$data['encrypted'];
167
-		$data['encrypted'] = (bool)$data['encrypted'];
164
+		$data['mtime'] = (int) $data['mtime'];
165
+		$data['storage_mtime'] = (int) $data['storage_mtime'];
166
+		$data['encryptedVersion'] = (int) $data['encrypted'];
167
+		$data['encrypted'] = (bool) $data['encrypted'];
168 168
 		$data['storage_id'] = $data['storage'];
169
-		$data['storage'] = (int)$data['storage'];
169
+		$data['storage'] = (int) $data['storage'];
170 170
 		$data['mimetype'] = $mimetypeLoader->getMimetypeById($data['mimetype']);
171 171
 		$data['mimepart'] = $mimetypeLoader->getMimetypeById($data['mimepart']);
172 172
 		if ($data['storage_mtime'] == 0) {
@@ -175,12 +175,12 @@  discard block
 block discarded – undo
175 175
 		if (isset($data['f_permissions'])) {
176 176
 			$data['scan_permissions'] ??= $data['f_permissions'];
177 177
 		}
178
-		$data['permissions'] = (int)$data['permissions'];
178
+		$data['permissions'] = (int) $data['permissions'];
179 179
 		if (isset($data['creation_time'])) {
180
-			$data['creation_time'] = (int)$data['creation_time'];
180
+			$data['creation_time'] = (int) $data['creation_time'];
181 181
 		}
182 182
 		if (isset($data['upload_time'])) {
183
-			$data['upload_time'] = (int)$data['upload_time'];
183
+			$data['upload_time'] = (int) $data['upload_time'];
184 184
 		}
185 185
 		return new CacheEntry($data);
186 186
 	}
@@ -216,7 +216,7 @@  discard block
 block discarded – undo
216 216
 			$files = $result->fetchAll();
217 217
 			$result->closeCursor();
218 218
 
219
-			return array_map(function (array $data) use ($metadataQuery) {
219
+			return array_map(function(array $data) use ($metadataQuery) {
220 220
 				$data['metadata'] = $metadataQuery->extractMetadata($data)->asArray();
221 221
 				return self::cacheEntryFromData($data, $this->mimetypeLoader);
222 222
 			}, $files);
@@ -273,7 +273,7 @@  discard block
 block discarded – undo
273 273
 			$data['parent'] = $this->getParentId($file);
274 274
 		}
275 275
 		if ($data['parent'] === -1 && $file !== '') {
276
-			throw new \Exception('Parent folder not in filecache for ' . $file);
276
+			throw new \Exception('Parent folder not in filecache for '.$file);
277 277
 		}
278 278
 		$data['name'] = basename($file);
279 279
 
@@ -355,7 +355,7 @@  discard block
 block discarded – undo
355 355
 			$query->update('filecache')
356 356
 				->whereFileId($id)
357 357
 				->whereStorageId($this->getNumericStorageId())
358
-				->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
358
+				->andWhere($query->expr()->orX(...array_map(function($key, $value) use ($query) {
359 359
 					return $query->expr()->orX(
360 360
 						$query->expr()->neq($key, $query->createNamedParameter($value)),
361 361
 						$query->expr()->isNull($key)
@@ -389,7 +389,7 @@  discard block
 block discarded – undo
389 389
 				$query->update('filecache_extended')
390 390
 					->whereFileId($id)
391 391
 					->hintShardKey('storage', $this->getNumericStorageId())
392
-					->andWhere($query->expr()->orX(...array_map(function ($key, $value) use ($query) {
392
+					->andWhere($query->expr()->orX(...array_map(function($key, $value) use ($query) {
393 393
 						return $query->expr()->orX(
394 394
 							$query->expr()->neq($key, $query->createNamedParameter($value)),
395 395
 							$query->expr()->isNull($key)
@@ -486,7 +486,7 @@  discard block
 block discarded – undo
486 486
 		$id = $result->fetchOne();
487 487
 		$result->closeCursor();
488 488
 
489
-		return $id === false ? -1 : (int)$id;
489
+		return $id === false ? -1 : (int) $id;
490 490
 	}
491 491
 
492 492
 	/**
@@ -500,7 +500,7 @@  discard block
 block discarded – undo
500 500
 			return -1;
501 501
 		} else {
502 502
 			$parent = $this->getParentPath($file);
503
-			return (int)$this->getId($parent);
503
+			return (int) $this->getId($parent);
504 504
 		}
505 505
 	}
506 506
 
@@ -569,10 +569,10 @@  discard block
 block discarded – undo
569 569
 		// and collecting all folder ids to later use to delete the filecache entries
570 570
 		while ($entryId = array_pop($queue)) {
571 571
 			$children = $this->getFolderContentsById($entryId);
572
-			$childIds = array_map(function (ICacheEntry $cacheEntry) {
572
+			$childIds = array_map(function(ICacheEntry $cacheEntry) {
573 573
 				return $cacheEntry->getId();
574 574
 			}, $children);
575
-			$childPaths = array_map(function (ICacheEntry $cacheEntry) {
575
+			$childPaths = array_map(function(ICacheEntry $cacheEntry) {
576 576
 				return $cacheEntry->getPath();
577 577
 			}, $children);
578 578
 
@@ -679,7 +679,7 @@  discard block
 block discarded – undo
679 679
 
680 680
 			$sourceData = $sourceCache->get($sourcePath);
681 681
 			if (!$sourceData) {
682
-				throw new \Exception('Source path not found in cache: ' . $sourcePath);
682
+				throw new \Exception('Source path not found in cache: '.$sourcePath);
683 683
 			}
684 684
 
685 685
 			$shardDefinition = $this->connection->getShardDefinition('filecache');
@@ -698,10 +698,10 @@  discard block
 block discarded – undo
698 698
 			[$targetStorageId, $targetPath] = $this->getMoveInfo($targetPath);
699 699
 
700 700
 			if (is_null($sourceStorageId) || $sourceStorageId === false) {
701
-				throw new \Exception('Invalid source storage id: ' . $sourceStorageId);
701
+				throw new \Exception('Invalid source storage id: '.$sourceStorageId);
702 702
 			}
703 703
 			if (is_null($targetStorageId) || $targetStorageId === false) {
704
-				throw new \Exception('Invalid target storage id: ' . $targetStorageId);
704
+				throw new \Exception('Invalid target storage id: '.$targetStorageId);
705 705
 			}
706 706
 
707 707
 			if ($sourceData['mimetype'] === 'httpd/unix-directory') {
@@ -810,7 +810,7 @@  discard block
 block discarded – undo
810 810
 		$query->select('fileid')
811 811
 			->from('filecache')
812 812
 			->where($query->expr()->eq('storage', $query->createNamedParameter($storageId, IQueryBuilder::PARAM_INT)))
813
-			->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($path) . '/%')));
813
+			->andWhere($query->expr()->like('path', $query->createNamedParameter($this->connection->escapeLikeParameter($path).'/%')));
814 814
 		return $query->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
815 815
 	}
816 816
 
@@ -856,7 +856,7 @@  discard block
 block discarded – undo
856 856
 		$result->closeCursor();
857 857
 
858 858
 		if ($size !== false) {
859
-			if ((int)$size === -1) {
859
+			if ((int) $size === -1) {
860 860
 				return self::SHALLOW;
861 861
 			} else {
862 862
 				return self::COMPLETE;
@@ -890,7 +890,7 @@  discard block
 block discarded – undo
890 890
 	 */
891 891
 	public function searchByMime($mimetype) {
892 892
 		if (!str_contains($mimetype, '/')) {
893
-			$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype . '/%');
893
+			$operator = new SearchComparison(ISearchComparison::COMPARE_LIKE, 'mimetype', $mimetype.'/%');
894 894
 		} else {
895 895
 			$operator = new SearchComparison(ISearchComparison::COMPARE_EQUAL, 'mimetype', $mimetype);
896 896
 		}
@@ -945,7 +945,7 @@  discard block
 block discarded – undo
945 945
 				->andWhere($query->expr()->eq('size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT)));
946 946
 
947 947
 			$result = $query->executeQuery();
948
-			$size = (int)$result->fetchOne();
948
+			$size = (int) $result->fetchOne();
949 949
 			$result->closeCursor();
950 950
 
951 951
 			return $size;
@@ -995,13 +995,13 @@  discard block
 block discarded – undo
995 995
 			$result->closeCursor();
996 996
 
997 997
 			if ($rows) {
998
-				$sizes = array_map(function (array $row) {
998
+				$sizes = array_map(function(array $row) {
999 999
 					return Util::numericToNumber($row['size']);
1000 1000
 				}, $rows);
1001
-				$unencryptedOnlySizes = array_map(function (array $row) {
1001
+				$unencryptedOnlySizes = array_map(function(array $row) {
1002 1002
 					return Util::numericToNumber($row['unencrypted_size']);
1003 1003
 				}, $rows);
1004
-				$unencryptedSizes = array_map(function (array $row) {
1004
+				$unencryptedSizes = array_map(function(array $row) {
1005 1005
 					return Util::numericToNumber(($row['unencrypted_size'] > 0) ? $row['unencrypted_size'] : $row['size']);
1006 1006
 				}, $rows);
1007 1007
 
@@ -1068,8 +1068,8 @@  discard block
 block discarded – undo
1068 1068
 		$files = $result->fetchAll(\PDO::FETCH_COLUMN);
1069 1069
 		$result->closeCursor();
1070 1070
 
1071
-		return array_map(function ($id) {
1072
-			return (int)$id;
1071
+		return array_map(function($id) {
1072
+			return (int) $id;
1073 1073
 		}, $files);
1074 1074
 	}
1075 1075
 
@@ -1095,7 +1095,7 @@  discard block
 block discarded – undo
1095 1095
 		$path = $result->fetchOne();
1096 1096
 		$result->closeCursor();
1097 1097
 
1098
-		return $path === false ? false : (string)$path;
1098
+		return $path === false ? false : (string) $path;
1099 1099
 	}
1100 1100
 
1101 1101
 	/**
@@ -1119,7 +1119,7 @@  discard block
 block discarded – undo
1119 1119
 			return null;
1120 1120
 		}
1121 1121
 
1122
-		return (string)$path;
1122
+		return (string) $path;
1123 1123
 	}
1124 1124
 
1125 1125
 	/**
@@ -1188,12 +1188,12 @@  discard block
 block discarded – undo
1188 1188
 
1189 1189
 		$fileId = $this->put($targetPath, $data);
1190 1190
 		if ($fileId <= 0) {
1191
-			throw new \RuntimeException('Failed to copy to ' . $targetPath . ' from cache with source data ' . json_encode($data) . ' ');
1191
+			throw new \RuntimeException('Failed to copy to '.$targetPath.' from cache with source data '.json_encode($data).' ');
1192 1192
 		}
1193 1193
 		if ($sourceEntry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) {
1194 1194
 			$folderContent = $sourceCache->getFolderContentsById($sourceEntry->getId());
1195 1195
 			foreach ($folderContent as $subEntry) {
1196
-				$subTargetPath = $targetPath . '/' . $subEntry->getName();
1196
+				$subTargetPath = $targetPath.'/'.$subEntry->getName();
1197 1197
 				$this->copyFromCache($sourceCache, $subEntry, $subTargetPath);
1198 1198
 			}
1199 1199
 		}
@@ -1267,7 +1267,7 @@  discard block
 block discarded – undo
1267 1267
 				$cacheItem['parent'] = $this->getParentId($targetPath);
1268 1268
 				$cacheItem['name'] = basename($cacheItem['path']);
1269 1269
 			} else {
1270
-				$cacheItem['path'] = $targetPath . '/' . substr($cacheItem['path'], $sourcePathLength + 1); // +1 for the leading slash
1270
+				$cacheItem['path'] = $targetPath.'/'.substr($cacheItem['path'], $sourcePathLength + 1); // +1 for the leading slash
1271 1271
 			}
1272 1272
 			$cacheItem['path_hash'] = md5($cacheItem['path']);
1273 1273
 			$cacheItem['storage'] = $this->getNumericStorageId();
Please login to merge, or discard this patch.
lib/private/Files/Cache/Wrapper/CachePermissionsMask.php 1 patch
Indentation   +19 added lines, -19 removed lines patch added patch discarded remove patch
@@ -8,25 +8,25 @@
 block discarded – undo
8 8
 namespace OC\Files\Cache\Wrapper;
9 9
 
10 10
 class CachePermissionsMask extends CacheWrapper {
11
-	/**
12
-	 * @var int
13
-	 */
14
-	protected $mask;
11
+    /**
12
+     * @var int
13
+     */
14
+    protected $mask;
15 15
 
16
-	/**
17
-	 * @param \OCP\Files\Cache\ICache $cache
18
-	 * @param int $mask
19
-	 */
20
-	public function __construct($cache, $mask) {
21
-		parent::__construct($cache);
22
-		$this->mask = $mask;
23
-	}
16
+    /**
17
+     * @param \OCP\Files\Cache\ICache $cache
18
+     * @param int $mask
19
+     */
20
+    public function __construct($cache, $mask) {
21
+        parent::__construct($cache);
22
+        $this->mask = $mask;
23
+    }
24 24
 
25
-	protected function formatCacheEntry($entry) {
26
-		if (isset($entry['permissions'])) {
27
-			$entry['scan_permissions'] ??= $entry['permissions'];
28
-			$entry['permissions'] &= $this->mask;
29
-		}
30
-		return $entry;
31
-	}
25
+    protected function formatCacheEntry($entry) {
26
+        if (isset($entry['permissions'])) {
27
+            $entry['scan_permissions'] ??= $entry['permissions'];
28
+            $entry['permissions'] &= $this->mask;
29
+        }
30
+        return $entry;
31
+    }
32 32
 }
Please login to merge, or discard this patch.
lib/private/Files/Storage/Wrapper/PermissionsMask.php 1 patch
Indentation   +116 added lines, -116 removed lines patch added patch discarded remove patch
@@ -19,120 +19,120 @@
 block discarded – undo
19 19
  * Note that the read permissions can't be masked
20 20
  */
21 21
 class PermissionsMask extends Wrapper {
22
-	/**
23
-	 * @var int the permissions bits we want to keep
24
-	 */
25
-	private $mask;
26
-
27
-	/**
28
-	 * @param array $parameters ['storage' => $storage, 'mask' => $mask]
29
-	 *
30
-	 * $storage: The storage the permissions mask should be applied on
31
-	 * $mask: The permission bits that should be kept, a combination of the \OCP\Constant::PERMISSION_ constants
32
-	 */
33
-	public function __construct(array $parameters) {
34
-		parent::__construct($parameters);
35
-		$this->mask = $parameters['mask'];
36
-	}
37
-
38
-	private function checkMask(int $permissions): bool {
39
-		return ($this->mask & $permissions) === $permissions;
40
-	}
41
-
42
-	public function isUpdatable(string $path): bool {
43
-		return $this->checkMask(Constants::PERMISSION_UPDATE) && parent::isUpdatable($path);
44
-	}
45
-
46
-	public function isCreatable(string $path): bool {
47
-		return $this->checkMask(Constants::PERMISSION_CREATE) && parent::isCreatable($path);
48
-	}
49
-
50
-	public function isDeletable(string $path): bool {
51
-		return $this->checkMask(Constants::PERMISSION_DELETE) && parent::isDeletable($path);
52
-	}
53
-
54
-	public function isSharable(string $path): bool {
55
-		return $this->checkMask(Constants::PERMISSION_SHARE) && parent::isSharable($path);
56
-	}
57
-
58
-	public function getPermissions(string $path): int {
59
-		return $this->storage->getPermissions($path) & $this->mask;
60
-	}
61
-
62
-	public function rename(string $source, string $target): bool {
63
-		//This is a rename of the transfer file to the original file
64
-		if (dirname($source) === dirname($target) && strpos($source, '.ocTransferId') > 0) {
65
-			return $this->checkMask(Constants::PERMISSION_CREATE) && parent::rename($source, $target);
66
-		}
67
-		return $this->checkMask(Constants::PERMISSION_UPDATE) && parent::rename($source, $target);
68
-	}
69
-
70
-	public function copy(string $source, string $target): bool {
71
-		return $this->checkMask(Constants::PERMISSION_CREATE) && parent::copy($source, $target);
72
-	}
73
-
74
-	public function touch(string $path, ?int $mtime = null): bool {
75
-		$permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE;
76
-		return $this->checkMask($permissions) && parent::touch($path, $mtime);
77
-	}
78
-
79
-	public function mkdir(string $path): bool {
80
-		return $this->checkMask(Constants::PERMISSION_CREATE) && parent::mkdir($path);
81
-	}
82
-
83
-	public function rmdir(string $path): bool {
84
-		return $this->checkMask(Constants::PERMISSION_DELETE) && parent::rmdir($path);
85
-	}
86
-
87
-	public function unlink(string $path): bool {
88
-		return $this->checkMask(Constants::PERMISSION_DELETE) && parent::unlink($path);
89
-	}
90
-
91
-	public function file_put_contents(string $path, mixed $data): int|float|false {
92
-		$permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE;
93
-		return $this->checkMask($permissions) ? parent::file_put_contents($path, $data) : false;
94
-	}
95
-
96
-	public function fopen(string $path, string $mode) {
97
-		if ($mode === 'r' || $mode === 'rb') {
98
-			return parent::fopen($path, $mode);
99
-		} else {
100
-			$permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE;
101
-			return $this->checkMask($permissions) ? parent::fopen($path, $mode) : false;
102
-		}
103
-	}
104
-
105
-	public function getCache(string $path = '', ?IStorage $storage = null): \OCP\Files\Cache\ICache {
106
-		if (!$storage) {
107
-			$storage = $this;
108
-		}
109
-		$sourceCache = parent::getCache($path, $storage);
110
-		return new CachePermissionsMask($sourceCache, $this->mask);
111
-	}
112
-
113
-	public function getMetaData(string $path): ?array {
114
-		$data = parent::getMetaData($path);
115
-
116
-		if ($data && isset($data['permissions'])) {
117
-			$data['scan_permissions'] ??= $data['permissions'];
118
-			$data['permissions'] &= $this->mask;
119
-		}
120
-		return $data;
121
-	}
122
-
123
-	public function getScanner(string $path = '', ?IStorage $storage = null): \OCP\Files\Cache\IScanner {
124
-		if (!$storage) {
125
-			$storage = $this->storage;
126
-		}
127
-		return parent::getScanner($path, $storage);
128
-	}
129
-
130
-	public function getDirectoryContent(string $directory): \Traversable {
131
-		foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
132
-			$data['scan_permissions'] ??= $data['permissions'];
133
-			$data['permissions'] &= $this->mask;
134
-
135
-			yield $data;
136
-		}
137
-	}
22
+    /**
23
+     * @var int the permissions bits we want to keep
24
+     */
25
+    private $mask;
26
+
27
+    /**
28
+     * @param array $parameters ['storage' => $storage, 'mask' => $mask]
29
+     *
30
+     * $storage: The storage the permissions mask should be applied on
31
+     * $mask: The permission bits that should be kept, a combination of the \OCP\Constant::PERMISSION_ constants
32
+     */
33
+    public function __construct(array $parameters) {
34
+        parent::__construct($parameters);
35
+        $this->mask = $parameters['mask'];
36
+    }
37
+
38
+    private function checkMask(int $permissions): bool {
39
+        return ($this->mask & $permissions) === $permissions;
40
+    }
41
+
42
+    public function isUpdatable(string $path): bool {
43
+        return $this->checkMask(Constants::PERMISSION_UPDATE) && parent::isUpdatable($path);
44
+    }
45
+
46
+    public function isCreatable(string $path): bool {
47
+        return $this->checkMask(Constants::PERMISSION_CREATE) && parent::isCreatable($path);
48
+    }
49
+
50
+    public function isDeletable(string $path): bool {
51
+        return $this->checkMask(Constants::PERMISSION_DELETE) && parent::isDeletable($path);
52
+    }
53
+
54
+    public function isSharable(string $path): bool {
55
+        return $this->checkMask(Constants::PERMISSION_SHARE) && parent::isSharable($path);
56
+    }
57
+
58
+    public function getPermissions(string $path): int {
59
+        return $this->storage->getPermissions($path) & $this->mask;
60
+    }
61
+
62
+    public function rename(string $source, string $target): bool {
63
+        //This is a rename of the transfer file to the original file
64
+        if (dirname($source) === dirname($target) && strpos($source, '.ocTransferId') > 0) {
65
+            return $this->checkMask(Constants::PERMISSION_CREATE) && parent::rename($source, $target);
66
+        }
67
+        return $this->checkMask(Constants::PERMISSION_UPDATE) && parent::rename($source, $target);
68
+    }
69
+
70
+    public function copy(string $source, string $target): bool {
71
+        return $this->checkMask(Constants::PERMISSION_CREATE) && parent::copy($source, $target);
72
+    }
73
+
74
+    public function touch(string $path, ?int $mtime = null): bool {
75
+        $permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE;
76
+        return $this->checkMask($permissions) && parent::touch($path, $mtime);
77
+    }
78
+
79
+    public function mkdir(string $path): bool {
80
+        return $this->checkMask(Constants::PERMISSION_CREATE) && parent::mkdir($path);
81
+    }
82
+
83
+    public function rmdir(string $path): bool {
84
+        return $this->checkMask(Constants::PERMISSION_DELETE) && parent::rmdir($path);
85
+    }
86
+
87
+    public function unlink(string $path): bool {
88
+        return $this->checkMask(Constants::PERMISSION_DELETE) && parent::unlink($path);
89
+    }
90
+
91
+    public function file_put_contents(string $path, mixed $data): int|float|false {
92
+        $permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE;
93
+        return $this->checkMask($permissions) ? parent::file_put_contents($path, $data) : false;
94
+    }
95
+
96
+    public function fopen(string $path, string $mode) {
97
+        if ($mode === 'r' || $mode === 'rb') {
98
+            return parent::fopen($path, $mode);
99
+        } else {
100
+            $permissions = $this->file_exists($path) ? Constants::PERMISSION_UPDATE : Constants::PERMISSION_CREATE;
101
+            return $this->checkMask($permissions) ? parent::fopen($path, $mode) : false;
102
+        }
103
+    }
104
+
105
+    public function getCache(string $path = '', ?IStorage $storage = null): \OCP\Files\Cache\ICache {
106
+        if (!$storage) {
107
+            $storage = $this;
108
+        }
109
+        $sourceCache = parent::getCache($path, $storage);
110
+        return new CachePermissionsMask($sourceCache, $this->mask);
111
+    }
112
+
113
+    public function getMetaData(string $path): ?array {
114
+        $data = parent::getMetaData($path);
115
+
116
+        if ($data && isset($data['permissions'])) {
117
+            $data['scan_permissions'] ??= $data['permissions'];
118
+            $data['permissions'] &= $this->mask;
119
+        }
120
+        return $data;
121
+    }
122
+
123
+    public function getScanner(string $path = '', ?IStorage $storage = null): \OCP\Files\Cache\IScanner {
124
+        if (!$storage) {
125
+            $storage = $this->storage;
126
+        }
127
+        return parent::getScanner($path, $storage);
128
+    }
129
+
130
+    public function getDirectoryContent(string $directory): \Traversable {
131
+        foreach ($this->getWrapperStorage()->getDirectoryContent($directory) as $data) {
132
+            $data['scan_permissions'] ??= $data['permissions'];
133
+            $data['permissions'] &= $this->mask;
134
+
135
+            yield $data;
136
+        }
137
+    }
138 138
 }
Please login to merge, or discard this patch.